mirror of
https://github.com/block/buzz.git
synced 2026-11-12 20:31:12 +00:00
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:
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user