Add optional standalone pairing relay to Helm chart (#1799)

Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Tyler
2026-07-13 11:36:04 -04:00
committed by GitHub
parent d38a7ef775
commit 9b47c8548f
15 changed files with 454 additions and 64 deletions

View File

@@ -6,7 +6,15 @@ use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let addr: SocketAddr = ([127, 0, 0, 1], 5000).into();
let addr_raw =
std::env::var("BUZZ_PAIR_RELAY_BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:5000".to_string());
let addr: SocketAddr = match addr_raw.parse() {
Ok(addr) => addr,
Err(e) => {
eprintln!("fatal: invalid BUZZ_PAIR_RELAY_BIND_ADDR {addr_raw:?}: {e}");
std::process::exit(1);
}
};
let listener = match TcpListener::bind(addr).await {
Ok(l) => l,
Err(e) => {

View File

@@ -33,6 +33,8 @@ pub struct Config {
pub redis_url: String,
/// Public WebSocket URL of this relay, advertised in NIP-11.
pub relay_url: String,
/// Public WebSocket URL of the dedicated device-pairing relay, when configured.
pub pairing_relay_url: Option<String>,
/// Maximum number of concurrent WebSocket connections.
pub max_connections: usize,
/// Maximum number of concurrently executing message handlers.
@@ -234,6 +236,25 @@ impl Config {
let relay_url =
std::env::var("RELAY_URL").unwrap_or_else(|_| "ws://localhost:3000".to_string());
let pairing_relay_url = std::env::var("BUZZ_PAIRING_RELAY_URL")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.map(|value| {
let parsed = url::Url::parse(&value).map_err(|e| {
ConfigError::InvalidValue(format!(
"BUZZ_PAIRING_RELAY_URL must be a valid ws:// or wss:// URL: {e}"
))
})?;
if !matches!(parsed.scheme(), "ws" | "wss") || parsed.host_str().is_none() {
return Err(ConfigError::InvalidValue(
"BUZZ_PAIRING_RELAY_URL must be a valid ws:// or wss:// URL".to_string(),
));
}
Ok(value)
})
.transpose()?;
let max_connections = std::env::var("BUZZ_MAX_CONNECTIONS")
.ok()
.and_then(|v| v.parse().ok())
@@ -508,6 +529,7 @@ impl Config {
database_url,
redis_url,
relay_url,
pairing_relay_url,
max_connections,
max_concurrent_handlers,
send_buffer_size,
@@ -676,6 +698,25 @@ mod tests {
));
}
#[test]
fn pairing_relay_url_accepts_websocket_urls_and_rejects_http() {
let _guard = ENV_MUTEX.lock().unwrap();
std::env::set_var("BUZZ_PAIRING_RELAY_URL", "wss://pairing.buzz.xyz");
let config = Config::from_env().expect("config");
assert_eq!(
config.pairing_relay_url.as_deref(),
Some("wss://pairing.buzz.xyz")
);
std::env::set_var("BUZZ_PAIRING_RELAY_URL", "https://pairing.buzz.xyz");
let result = Config::from_env();
std::env::remove_var("BUZZ_PAIRING_RELAY_URL");
assert!(matches!(
result,
Err(ConfigError::InvalidValue(ref msg)) if msg.contains("BUZZ_PAIRING_RELAY_URL")
));
}
#[test]
fn max_frame_bytes_can_be_configured() {
let _guard = ENV_MUTEX.lock().unwrap();

View File

@@ -46,6 +46,9 @@ pub struct RelayInfo {
pub version: String,
/// Protocol and resource limits advertised to clients.
pub limitation: Option<RelayLimitation>,
/// Public WebSocket URL of the dedicated NIP-AB device-pairing relay.
#[serde(skip_serializing_if = "Option::is_none")]
pub pairing_relay_url: Option<String>,
/// Relay's own signing pubkey (NIP-11 `self` field, NIP-43).
#[serde(rename = "self", skip_serializing_if = "Option::is_none")]
pub relay_self: Option<String>,
@@ -132,6 +135,7 @@ impl RelayInfo {
icon: Option<&str>,
advertise_nip43: bool,
max_message_length: usize,
pairing_relay_url: Option<&str>,
) -> Self {
debug_assert!(
!advertise_nip43 || relay_self.is_some(),
@@ -154,6 +158,7 @@ impl RelayInfo {
software: "https://github.com/block/buzz".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
limitation: Some(relay_limitation(max_message_length)),
pairing_relay_url: pairing_relay_url.map(str::to_string),
relay_self: relay_self.map(|s| s.to_string()),
}
}
@@ -185,6 +190,7 @@ pub(crate) async fn nip11_document(state: &crate::state::AppState, raw_host: &st
icon.as_deref(),
advertise_nip43,
state.config.max_frame_bytes,
state.config.pairing_relay_url.as_deref(),
)
}
@@ -248,11 +254,13 @@ pub(crate) fn nip11_facts(state: &crate::state::AppState) -> (Option<String>, bo
/// hard build break, the same way a deny-lint would. If you must change this
/// signature, you are changing the conformance contract: update the conformance
/// doc and prove the new input is host-scoped, not unscoped, first.
#[allow(clippy::type_complexity)]
const _RELAY_INFO_BUILD_STATIC_INPUT_FENCE: fn(
Option<&str>,
Option<&str>,
bool,
usize,
Option<&str>,
) -> RelayInfo = RelayInfo::build;
#[cfg(test)]
@@ -291,10 +299,31 @@ mod tests {
#[test]
fn build_advertises_buzz_repository_url() {
let info = RelayInfo::build(None, None, false, DEFAULT_MAX_FRAME_BYTES);
let info = RelayInfo::build(None, None, false, DEFAULT_MAX_FRAME_BYTES, None);
assert_eq!(info.software, "https://github.com/block/buzz");
}
#[test]
fn configured_pairing_relay_is_advertised_and_unset_value_is_omitted() {
let info = RelayInfo::build(
None,
None,
false,
DEFAULT_MAX_FRAME_BYTES,
Some("wss://pairing.buzz.xyz"),
);
let json = serde_json::to_value(&info).expect("serialize");
assert_eq!(
json.get("pairing_relay_url")
.and_then(|value| value.as_str()),
Some("wss://pairing.buzz.xyz")
);
let info = RelayInfo::build(None, None, false, DEFAULT_MAX_FRAME_BYTES, None);
let json = serde_json::to_value(&info).expect("serialize");
assert!(json.get("pairing_relay_url").is_none());
}
/// NIP-WP → NIP-11 mirror: a set workspace icon is served in the standard
/// `icon` field; no icon (or a cleared, empty icon) omits the field
/// entirely so the JSON matches pre-icon documents byte-for-byte.
@@ -305,6 +334,7 @@ mod tests {
Some("data:image/webp;base64,UklGRg=="),
false,
DEFAULT_MAX_FRAME_BYTES,
None,
);
assert_eq!(
info.icon.as_deref(),
@@ -317,7 +347,7 @@ mod tests {
);
for icon in [None, Some("")] {
let info = RelayInfo::build(None, icon, false, DEFAULT_MAX_FRAME_BYTES);
let info = RelayInfo::build(None, icon, false, DEFAULT_MAX_FRAME_BYTES, None);
assert!(info.icon.is_none());
let json = serde_json::to_value(&info).expect("serialize");
assert!(
@@ -337,7 +367,7 @@ mod tests {
#[test]
fn max_message_length_uses_configured_frame_limit() {
let info = RelayInfo::build(None, None, false, 262_144);
let info = RelayInfo::build(None, None, false, 262_144, None);
let limitation = info.limitation.expect("limitation");
assert_eq!(limitation.max_message_length, Some(262_144));
}
@@ -368,7 +398,7 @@ mod tests {
/// Open relay, ephemeral key — both `self` and NIP-43 are absent.
#[test]
fn build_open_relay_ephemeral_key_omits_self_and_nip43() {
let info = RelayInfo::build(None, None, false, DEFAULT_MAX_FRAME_BYTES);
let info = RelayInfo::build(None, None, false, DEFAULT_MAX_FRAME_BYTES, None);
assert!(info.relay_self.is_none());
assert!(!info.supported_nips.contains(&NIP_RELAY_MEMBERSHIP));
}
@@ -381,7 +411,7 @@ mod tests {
#[test]
fn build_open_relay_stable_key_advertises_self_but_not_nip43() {
let pk = "0000000000000000000000000000000000000000000000000000000000000001";
let info = RelayInfo::build(Some(pk), None, false, DEFAULT_MAX_FRAME_BYTES);
let info = RelayInfo::build(Some(pk), None, false, DEFAULT_MAX_FRAME_BYTES, None);
assert_eq!(info.relay_self.as_deref(), Some(pk));
assert!(!info.supported_nips.contains(&NIP_RELAY_MEMBERSHIP));
}
@@ -390,7 +420,7 @@ mod tests {
#[test]
fn build_membership_relay_advertises_self_and_nip43() {
let pk = "0000000000000000000000000000000000000000000000000000000000000001";
let info = RelayInfo::build(Some(pk), None, true, DEFAULT_MAX_FRAME_BYTES);
let info = RelayInfo::build(Some(pk), None, true, DEFAULT_MAX_FRAME_BYTES, None);
assert_eq!(info.relay_self.as_deref(), Some(pk));
assert!(info.supported_nips.contains(&NIP_RELAY_MEMBERSHIP));
}
@@ -401,6 +431,6 @@ mod tests {
#[test]
#[should_panic(expected = "advertise_nip43=true requires relay_self=Some")]
fn build_nip43_without_self_panics_in_debug() {
let _ = RelayInfo::build(None, None, true, DEFAULT_MAX_FRAME_BYTES);
let _ = RelayInfo::build(None, None, true, DEFAULT_MAX_FRAME_BYTES, None);
}
}