tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit 23fb3e1ceba3fec8fadfad293ede580b1e6f91fb
parent db67342c7416e1e4b03bd20ed8f17e0cf02caa44
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 16:05:37 -0700

query: reject unsupported search filters

Diffstat:
Mcrates/tangle_runtime/src/relay/core.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/tangle_runtime/src/runtime.rs | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/tangle_runtime/src/server.rs | 14++++++++++++++
Mcrates/tangle_runtime/src/session.rs | 4++++
4 files changed, 139 insertions(+), 0 deletions(-)

diff --git a/crates/tangle_runtime/src/relay/core.rs b/crates/tangle_runtime/src/relay/core.rs @@ -412,6 +412,19 @@ impl BaseRelayLimits { } impl BaseRelay { + pub(crate) fn unsupported_search_closed( + subscription_id: &SubscriptionId, + filters: &[Filter], + ) -> Option<RelayMessage> { + filters + .iter() + .any(|filter| filter.search().is_some()) + .then(|| RelayMessage::Closed { + subscription_id: subscription_id.clone(), + message: "unsupported: search filters are not supported".to_owned(), + }) + } + pub fn open( config: &PocketStoreConfig, limits: BaseRelayLimits, @@ -770,6 +783,9 @@ impl BaseRelay { ) -> Result<BaseRelayQueryReport, BaseRelayError> { self.limits.validate_subscription_id(&subscription_id)?; self.limits.validate_filters(&filters)?; + if let Some(message) = Self::unsupported_search_closed(&subscription_id, &filters) { + return Ok(BaseRelayQueryReport::new(vec![message], false)); + } self.subscriptions .subscribe(subscription_id.clone(), filters.clone(), auth.clone())?; self.query_req_with_group_auth_report(subscription_id, filters, auth) @@ -783,6 +799,9 @@ impl BaseRelay { ) -> Result<BaseRelayQueryReport, BaseRelayError> { self.limits.validate_subscription_id(&subscription_id)?; self.limits.validate_filters(&filters)?; + if let Some(message) = Self::unsupported_search_closed(&subscription_id, &filters) { + return Ok(BaseRelayQueryReport::new(vec![message], false)); + } let report = self.query_events_report(&filters, auth)?; let group_read_denied = report.group_read_denied; let mut messages = report @@ -850,6 +869,9 @@ impl BaseRelay { ) -> Result<BaseRelayCountReport, BaseRelayError> { self.limits.validate_subscription_id(&subscription_id)?; self.limits.validate_filters(&filters)?; + if let Some(message) = Self::unsupported_search_closed(&subscription_id, &filters) { + return Ok(BaseRelayCountReport::new(message, false)); + } let report = self.count_events_report(&filters, auth)?; Ok(BaseRelayCountReport::new( RelayMessage::Count { @@ -1145,6 +1167,38 @@ mod tests { } #[test] + fn base_relay_rejects_search_req_and_count_as_unsupported() { + let mut relay = test_relay("base-relay-search-unsupported", 4); + let req_id = SubscriptionId::new("search-req").expect("req"); + let count_id = SubscriptionId::new("search-count").expect("count"); + let search = filter_from_value(&serde_json::json!({ + "search": "fresh carrots", + "limit": 1 + })) + .expect("filter"); + + assert_eq!( + relay + .handle_req(req_id.clone(), vec![search.clone()]) + .expect("req"), + vec![RelayMessage::Closed { + subscription_id: req_id, + message: "unsupported: search filters are not supported".to_owned() + }] + ); + assert_eq!(relay.active_subscription_count(), 0); + assert_eq!( + relay + .handle_count(count_id.clone(), vec![search]) + .expect("count"), + RelayMessage::Closed { + subscription_id: count_id, + message: "unsupported: search filters are not supported".to_owned() + } + ); + } + + #[test] fn base_relay_fetches_events_by_store_offset() { let relay = test_relay("base-relay-offset-lookup", 4); let event = signed_public_event(7, 1, Vec::new(), "offset"); diff --git a/crates/tangle_runtime/src/runtime.rs b/crates/tangle_runtime/src/runtime.rs @@ -597,6 +597,14 @@ impl TangleRuntimeHandle { .limits() .base_relay_limits() .validate_filters(&filters)?; + if let Some(message) = + BaseRelay::unsupported_search_closed(&subscription_id, &filters) + { + runtime + .metrics() + .record_query_latency(elapsed_micros(started_at)); + return Ok(vec![message]); + } if let Some(message) = runtime.rate_limit_req( &subscription_id, &filters, @@ -635,6 +643,14 @@ impl TangleRuntimeHandle { .limits() .base_relay_limits() .validate_filters(&filters)?; + if let Some(message) = + BaseRelay::unsupported_search_closed(&subscription_id, &filters) + { + runtime + .metrics() + .record_query_latency(elapsed_micros(started_at)); + return Ok(vec![message]); + } if let Some(message) = runtime.rate_limit_count( &subscription_id, &filters, @@ -2536,6 +2552,57 @@ mod tests { } #[tokio::test] + async fn runtime_rejects_search_req_and_count_as_unsupported() { + let root = temp_root("runtime-search-unsupported"); + let _ = std::fs::remove_dir_all(&root); + let handle = TangleRuntimeHandle::new( + TangleRuntime::open(runtime_config(&root, 8)).expect("runtime"), + ); + let mut auth = handle.auth_state().await.expect("auth"); + let req_id = SubscriptionId::new("search-req").expect("req"); + let count_id = SubscriptionId::new("search-count").expect("count"); + let search = + filter_from_value(&json!({"search": "fresh carrots", "limit": 1})).expect("filter"); + + assert_eq!( + handle + .handle_client_message( + ClientMessage::Req { + subscription_id: req_id.clone(), + filters: vec![search.clone()] + }, + &mut auth, + UnixTimestamp::new(1_714_124_433) + ) + .await + .expect("req"), + vec![RelayMessage::Closed { + subscription_id: req_id, + message: "unsupported: search filters are not supported".to_owned() + }] + ); + assert_eq!( + handle + .handle_client_message( + ClientMessage::Count { + subscription_id: count_id.clone(), + filters: vec![search] + }, + &mut auth, + UnixTimestamp::new(1_714_124_434) + ) + .await + .expect("count"), + vec![RelayMessage::Closed { + subscription_id: count_id, + message: "unsupported: search filters are not supported".to_owned() + }] + ); + + let _ = std::fs::remove_dir_all(root); + } + + #[tokio::test] async fn runtime_rate_limits_count_filter_kinds() { let root = temp_root("runtime-count-kind-rate-limit"); let _ = std::fs::remove_dir_all(&root); diff --git a/crates/tangle_runtime/src/server.rs b/crates/tangle_runtime/src/server.rs @@ -359,6 +359,20 @@ mod tests { json!(["EOSE", "sub-a"]) ); + send_client_value( + &mut socket, + json!(["REQ", "sub-search", {"search": "fresh carrots", "limit": 1}]), + ) + .await; + assert_eq!( + read_relay_value(&mut socket).await, + json!([ + "CLOSED", + "sub-search", + "unsupported: search filters are not supported" + ]) + ); + send_client_value(&mut socket, json!(["AUTH", event_to_value(&owner_auth)])).await; assert_eq!( read_relay_value(&mut socket).await, diff --git a/crates/tangle_runtime/src/session.rs b/crates/tangle_runtime/src/session.rs @@ -6,6 +6,7 @@ use crate::{ logging, relay::{ auth::{BaseAuthState, generate_auth_challenge}, + core::BaseRelay, live::{CloseResult, LiveSubscriptionSet}, }, runtime::{ @@ -320,6 +321,9 @@ impl TangleWebSocketSession { .base_relay_limits() .validate_subscription_id(&subscription_id)?; self.limits.base_relay_limits().validate_filters(&filters)?; + if let Some(message) = BaseRelay::unsupported_search_closed(&subscription_id, &filters) { + return Ok(vec![message]); + } if let Some(message) = self .runtime .rate_limit_req(