Files
buzz/crates/buzz-core/src/relay.rs
T
+2 61cc738ee8 feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: npub1hwqy0rnujtl25dzmlhn8qwux4kr8sjhas3ugltx9j5dm5dwkp2dsqjhytw <bb80478e7c92feaa345bfde6703b86ad86784afd84788facc5951bba35d60a9b@buzz.block.builderlab.xyz>
2026-07-22 13:24:46 -07:00

122 lines
4.3 KiB
Rust

//! Canonical relay identities shared by runtime components.
use thiserror::Error;
use url::{Host, Url};
/// Errors returned while canonicalizing a relay URL for runtime identity.
#[derive(Debug, Error, PartialEq, Eq)]
pub enum NormalizeRelayUrlError {
/// The input is not a valid URL.
#[error("invalid relay URL: {0}")]
InvalidUrl(String),
/// Relay sockets must use WebSocket schemes.
#[error("relay URL scheme must be ws or wss")]
InvalidScheme,
/// Relay identity never includes user credentials.
#[error("relay URL must not contain credentials")]
Credentials,
/// Relay identity never includes a fragment.
#[error("relay URL must not contain a fragment")]
Fragment,
/// A relay URL requires a host.
#[error("relay URL must contain a host")]
MissingHost,
}
/// Canonicalize a WebSocket relay URL for use as a runtime identity key.
///
/// This is the sole normalizer for `(agent, relay)` process identity. It keeps
/// the WebSocket scheme, lowercases DNS hosts, folds all loopback spellings to
/// `127.0.0.1`, removes default ports and a root slash, and preserves non-root
/// paths and queries. It deliberately is **not** the NIP-42 AUTH comparison
/// helper in `buzz-auth`: AUTH validation is a security boundary with narrower
/// equivalence rules and must not be widened by runtime-key canonicalization.
///
/// Connection code may retain the configured URL; this canonical form is for
/// identity, receipts, status and deduplication.
pub fn normalize_relay_url(raw: &str) -> Result<String, NormalizeRelayUrlError> {
let mut url = Url::parse(raw.trim())
.map_err(|error| NormalizeRelayUrlError::InvalidUrl(error.to_string()))?;
if !matches!(url.scheme(), "ws" | "wss") {
return Err(NormalizeRelayUrlError::InvalidScheme);
}
if !url.username().is_empty() || url.password().is_some() {
return Err(NormalizeRelayUrlError::Credentials);
}
if url.fragment().is_some() {
return Err(NormalizeRelayUrlError::Fragment);
}
let host = url.host().ok_or(NormalizeRelayUrlError::MissingHost)?;
let loopback = match host {
Host::Domain(domain) => domain.eq_ignore_ascii_case("localhost"),
Host::Ipv4(address) => address.is_loopback(),
Host::Ipv6(address) => address.is_loopback(),
};
if loopback {
url.set_host(Some("127.0.0.1"))
.map_err(|_| NormalizeRelayUrlError::MissingHost)?;
} else if let Host::Domain(domain) = host {
let lowercase = domain.to_ascii_lowercase();
url.set_host(Some(&lowercase))
.map_err(|_| NormalizeRelayUrlError::MissingHost)?;
}
let default_port = match url.scheme() {
"ws" => Some(80),
"wss" => Some(443),
_ => None,
};
if url.port() == default_port {
url.set_port(None)
.map_err(|_| NormalizeRelayUrlError::InvalidScheme)?;
}
if url.path() == "/" {
url.set_path("");
}
Ok(url.to_string().trim_end_matches('/').to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loopback_spellings_have_one_identity() {
let ipv6 = normalize_relay_url("wss://[::1]/").unwrap();
let ipv4 = normalize_relay_url("wss://127.0.0.1/").unwrap();
let localhost = normalize_relay_url("wss://localhost/").unwrap();
assert_eq!(ipv6, ipv4);
assert_eq!(ipv4, localhost);
assert_eq!(localhost, "wss://127.0.0.1");
}
#[test]
fn canonicalizes_only_identity_equivalences() {
assert_eq!(
normalize_relay_url(" WSS://Relay.Example:443/ ").unwrap(),
"wss://relay.example"
);
assert_eq!(
normalize_relay_url("ws://relay.example:8080/community/?x=1").unwrap(),
"ws://relay.example:8080/community/?x=1"
);
}
#[test]
fn rejects_non_relay_and_ambiguous_urls() {
assert_eq!(
normalize_relay_url("https://relay.example").unwrap_err(),
NormalizeRelayUrlError::InvalidScheme
);
assert_eq!(
normalize_relay_url("wss://user@relay.example").unwrap_err(),
NormalizeRelayUrlError::Credentials
);
assert_eq!(
normalize_relay_url("wss://relay.example/#x").unwrap_err(),
NormalizeRelayUrlError::Fragment
);
}
}