From 17b9e8740b702c00f3910b61f9ee34c38994c158 Mon Sep 17 00:00:00 2001 From: Taksh Date: Wed, 26 Aug 2026 18:10:34 +0530 Subject: [PATCH] fix(cli): canonicalize the channel id channel and message queries filter on validate_uuid checks that the argument parses and throws the result away. Uuid::parse_str accepts uppercase, the unhyphenated 32-character form, braces and a urn:uuid: prefix, so all four reach the filter unchanged. Every h and d tag in the tree is written in the canonical lowercase hyphenated form, and a NIP-01 generic tag filter compares tag values byte for byte. A non-canonical spelling therefore matches nothing, and each of these commands prints an empty result rather than an error: channels get, channels members, channels canvas, messages list, messages thread The send path is unaffected: it parses the argument into a Uuid and hands that to the SDK builders, which write the canonical form. The one place that leaked into a send is the mention preflight, which filtered on the raw string and so failed with "could not load channel membership" instead of the parse error the input deserves. Parse once and filter on the canonical form. messages thread already had the parsed value to hand and was still building its filters from the raw argument. Signed-off-by: Taksh --- crates/buzz-cli/src/commands/channels.rs | 17 +++++++--- crates/buzz-cli/src/commands/messages.rs | 21 ++++++++++-- crates/buzz-cli/src/validate.rs | 43 ++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/crates/buzz-cli/src/commands/channels.rs b/crates/buzz-cli/src/commands/channels.rs index 7ad051ef9fc..15ad4039001 100644 --- a/crates/buzz-cli/src/commands/channels.rs +++ b/crates/buzz-cli/src/commands/channels.rs @@ -11,7 +11,7 @@ use crate::client::{ use crate::commands::agents::fetch_archived_snapshot; use crate::commands::channel_templates::{self, ChannelTemplateRecord, TemplateAgentRoster}; use crate::error::CliError; -use crate::validate::{parse_uuid, read_or_stdin, validate_hex64, validate_uuid}; +use crate::validate::{parse_uuid, read_or_stdin, validate_hex64}; fn extract_channel_metadata(e: &serde_json::Value) -> serde_json::Value { serde_json::json!({ @@ -226,7 +226,14 @@ fn name_matches(name: &str, needle_lower: &str, exact: bool) -> bool { } pub async fn cmd_get_channel(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> { - validate_uuid(channel_id)?; + // Canonicalize before filtering. `validate_uuid` only checks that the input + // parses and throws the result away, but `Uuid::parse_str` accepts + // uppercase, the unhyphenated 32-character form, braces, and a `urn:uuid:` + // prefix. Every `d`/`h` tag in the tree is written in the canonical + // lowercase hyphenated form and a NIP-01 generic tag filter compares tag + // values byte for byte, so any other spelling matches nothing and the + // command prints an empty result instead of an error. + let channel_id = parse_uuid(channel_id)?.hyphenated().to_string(); let filter = serde_json::json!({ "kinds": [39000], "#d": [channel_id], @@ -249,7 +256,8 @@ pub async fn cmd_list_channel_members( client: &BuzzClient, channel_id: &str, ) -> Result<(), CliError> { - validate_uuid(channel_id)?; + // Same canonicalization as `cmd_get_channel`; see the note there. + let channel_id = parse_uuid(channel_id)?.hyphenated().to_string(); let filter = serde_json::json!({ "kinds": [39002], "#d": [channel_id], @@ -264,7 +272,8 @@ pub async fn cmd_list_channel_members( } pub async fn cmd_get_canvas(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> { - validate_uuid(channel_id)?; + // Same canonicalization as `cmd_get_channel`; see the note there. + let channel_id = parse_uuid(channel_id)?.hyphenated().to_string(); let filter = serde_json::json!({ "kinds": [40100], "#h": [channel_id] diff --git a/crates/buzz-cli/src/commands/messages.rs b/crates/buzz-cli/src/commands/messages.rs index ea273336e38..a5325efbfa2 100644 --- a/crates/buzz-cli/src/commands/messages.rs +++ b/crates/buzz-cli/src/commands/messages.rs @@ -6,7 +6,7 @@ use crate::client::{normalize_events, normalize_write_response, BuzzClient}; use crate::error::CliError; use crate::validate::{ infer_language, parse_event_id, parse_uuid, read_or_stdin, truncate_diff, - validate_content_size, validate_hex64, validate_uuid, MAX_DIFF_BYTES, + validate_content_size, validate_hex64, MAX_DIFF_BYTES, }; use buzz_sdk::mentions::{ extract_at_mentions_with_known, extract_nostr_uris, strip_code_regions, MENTION_CAP, @@ -168,6 +168,13 @@ async fn resolve_content_mentions( return Ok((vec![], vec![])); } + // `cmd_send_message` hands us the raw `--channel` argument. The send path + // itself is safe (the SDK builders take a parsed `Uuid` and write the + // canonical form), but this preflight filters on the string, so a + // non-canonical spelling loads no roster and the send fails with + // "could not load channel membership" rather than with the parse error the + // input deserves. + let channel_id = parse_uuid(channel_id)?.hyphenated().to_string(); let members_filter = serde_json::json!({ "kinds": [39002], "#d": [channel_id], @@ -362,7 +369,14 @@ pub async fn cmd_get_messages( kinds: Option<&str>, format: &crate::OutputFormat, ) -> Result<(), CliError> { - validate_uuid(channel_id)?; + // Canonicalize before filtering. `validate_uuid` only checks that the input + // parses and throws the result away, but `Uuid::parse_str` accepts + // uppercase, the unhyphenated 32-character form, braces, and a `urn:uuid:` + // prefix. Every `h` tag in the tree is written in the canonical lowercase + // hyphenated form and a NIP-01 generic tag filter compares tag values byte + // for byte, so any other spelling matches nothing and the command prints an + // empty list instead of an error. + let channel_id = parse_uuid(channel_id)?.hyphenated().to_string(); let limit = limit.unwrap_or(50).min(200); let mut filter = serde_json::json!({ @@ -427,6 +441,9 @@ pub async fn cmd_get_thread( format: &crate::OutputFormat, ) -> Result<(), CliError> { let expected_channel_id = parse_uuid(channel_id)?; + // The parsed value already exists here; the filters below were still built + // from the raw argument. See `cmd_get_messages` for why that matters. + let channel_id = expected_channel_id.hyphenated().to_string(); validate_hex64(event_id)?; let selected_event = fetch_event(client, event_id).await?; let root_event_id = resolve_thread_target( diff --git a/crates/buzz-cli/src/validate.rs b/crates/buzz-cli/src/validate.rs index 4985b441417..dfeaa70b04d 100644 --- a/crates/buzz-cli/src/validate.rs +++ b/crates/buzz-cli/src/validate.rs @@ -220,6 +220,49 @@ mod tests { assert!(matches!(err, CliError::Usage(_))); } + /// `validate_uuid` passes four spellings of the same id, and only one of + /// them is what the tree writes into an `h` or `d` tag. A caller that + /// validates and then filters on the raw argument therefore sends a tag + /// value that a NIP-01 generic tag filter — which compares byte for byte — + /// matches nothing against, and prints an empty result instead of an error. + #[test] + fn validate_uuid_accepts_non_canonical_spellings() { + for spelling in [ + "550E8400-E29B-41D4-A716-446655440000", + "550e8400e29b41d4a716446655440000", + "{550e8400-e29b-41d4-a716-446655440000}", + "urn:uuid:550e8400-e29b-41d4-a716-446655440000", + ] { + assert!( + validate_uuid(spelling).is_ok(), + "validate_uuid rejected {spelling}, so the canonicalization below is unnecessary" + ); + assert_ne!( + spelling, "550e8400-e29b-41d4-a716-446655440000", + "test data error: {spelling} is already canonical" + ); + } + } + + /// The canonicalization the query paths apply: parse, then render the one + /// form the relay stores. Every spelling above collapses onto it. + #[test] + fn parse_uuid_hyphenated_is_the_canonical_tag_value() { + for spelling in [ + "550e8400-e29b-41d4-a716-446655440000", + "550E8400-E29B-41D4-A716-446655440000", + "550e8400e29b41d4a716446655440000", + "{550e8400-e29b-41d4-a716-446655440000}", + "urn:uuid:550e8400-e29b-41d4-a716-446655440000", + ] { + assert_eq!( + parse_uuid(spelling).unwrap().hyphenated().to_string(), + "550e8400-e29b-41d4-a716-446655440000", + "{spelling} did not canonicalize" + ); + } + } + // --- validate_hex64 --- #[test]