commit 34c47f2e83f4a4f9977d1cb685ee5f181c5e087b
parent b8992e9381892358079e3042ba91c07cf8a3fc21
Author: triesap <tyson@radroots.org>
Date: Mon, 15 Jun 2026 14:20:02 -0700
runtime: parse event auth through Pocket
Diffstat:
4 files changed, 105 insertions(+), 5 deletions(-)
diff --git a/crates/tangle_runtime/Cargo.toml b/crates/tangle_runtime/Cargo.toml
@@ -12,7 +12,7 @@ axum = { version = "0.8", features = ["ws"] }
getrandom = "0.3"
http = "1"
serde = { version = "1", features = ["derive"] }
-serde_json = "1"
+serde_json = { version = "1", features = ["raw_value"] }
tangle_crypto = { path = "../tangle_crypto" }
tangle_groups = { path = "../tangle_groups" }
tangle_protocol = { path = "../tangle_protocol" }
diff --git a/crates/tangle_runtime/src/client_message.rs b/crates/tangle_runtime/src/client_message.rs
@@ -0,0 +1,100 @@
+#![forbid(unsafe_code)]
+
+use crate::{errors::BaseRelayError, pocket_conversion::pocket_event_to_tangle};
+use serde_json::value::RawValue;
+use tangle_protocol::{ClientMessage, parse_client_message};
+use tangle_store_pocket::parse_pocket_event_json;
+
+pub(crate) fn parse_runtime_client_message(raw: &str) -> Result<ClientMessage, String> {
+ let Ok(values) = serde_json::from_str::<Vec<Box<RawValue>>>(raw) else {
+ return parse_client_message(raw);
+ };
+ let Some(command) = values
+ .first()
+ .and_then(|value| serde_json::from_str::<String>(value.get()).ok())
+ else {
+ return parse_client_message(raw);
+ };
+ match command.as_str() {
+ "EVENT" => parse_event_or_auth(&values, ClientMessage::Event),
+ "AUTH" => parse_event_or_auth(&values, ClientMessage::Auth),
+ _ => parse_client_message(raw),
+ }
+}
+
+fn parse_event_or_auth(
+ values: &[Box<RawValue>],
+ build: impl FnOnce(tangle_protocol::Event) -> ClientMessage,
+) -> Result<ClientMessage, String> {
+ if values.len() != 2 {
+ return Err("EVENT and AUTH client messages must contain exactly one event".to_owned());
+ }
+ let event = parse_pocket_event_json(values[1].get().as_bytes())
+ .map_err(|error| error.message().to_owned())
+ .and_then(|event| pocket_event_to_tangle(&event).map_err(base_relay_error_message))?;
+ Ok(build(event))
+}
+
+fn base_relay_error_message(error: BaseRelayError) -> String {
+ error.message().to_owned()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::parse_runtime_client_message;
+ use serde_json::json;
+ use tangle_protocol::{ClientMessage, event_to_value};
+ use tangle_test_support::{FixtureKey, tangle_v2_auth_event, tangle_v2_event};
+
+ #[test]
+ fn runtime_parser_maps_event_and_auth_through_pocket_event_json() {
+ let event = tangle_v2_event(FixtureKey::Member, 1_714_124_433, 1, Vec::new(), "hello")
+ .expect("event");
+ assert_eq!(
+ parse_runtime_client_message(&json!(["EVENT", event_to_value(&event)]).to_string())
+ .expect("event"),
+ ClientMessage::Event(event.clone())
+ );
+
+ let auth =
+ tangle_v2_auth_event(FixtureKey::Member, "challenge-a", 1_714_124_434).expect("auth");
+ assert_eq!(
+ parse_runtime_client_message(&json!(["AUTH", event_to_value(&auth)]).to_string())
+ .expect("auth"),
+ ClientMessage::Auth(auth)
+ );
+ }
+
+ #[test]
+ fn runtime_parser_rejects_malformed_event_and_auth_payloads() {
+ assert_eq!(
+ parse_runtime_client_message("[\"EVENT\"]").expect_err("missing event"),
+ "EVENT and AUTH client messages must contain exactly one event"
+ );
+ assert_eq!(
+ parse_runtime_client_message("[\"AUTH\",{},{}]").expect_err("too many auth values"),
+ "EVENT and AUTH client messages must contain exactly one event"
+ );
+ assert!(
+ !parse_runtime_client_message("[\"EVENT\",{\"id\":5}]")
+ .expect_err("invalid event")
+ .is_empty()
+ );
+ }
+
+ #[test]
+ fn runtime_parser_delegates_req_count_and_close_until_filter_slice() {
+ assert!(matches!(
+ parse_runtime_client_message("[\"REQ\",\"sub\",{\"kinds\":[1]}]").expect("req"),
+ ClientMessage::Req { .. }
+ ));
+ assert!(matches!(
+ parse_runtime_client_message("[\"COUNT\",\"sub\",{\"kinds\":[1]}]").expect("count"),
+ ClientMessage::Count { .. }
+ ));
+ assert!(matches!(
+ parse_runtime_client_message("[\"CLOSE\",\"sub\"]").expect("close"),
+ ClientMessage::Close(_)
+ ));
+ }
+}
diff --git a/crates/tangle_runtime/src/lib.rs b/crates/tangle_runtime/src/lib.rs
@@ -1,5 +1,6 @@
#![forbid(unsafe_code)]
+pub(crate) mod client_message;
pub mod config;
pub mod errors;
pub mod event_bus;
diff --git a/crates/tangle_runtime/src/session.rs b/crates/tangle_runtime/src/session.rs
@@ -1,6 +1,7 @@
#![forbid(unsafe_code)]
use crate::{
+ client_message::parse_runtime_client_message,
errors::BaseRelayError,
event_bus::{TangleEventReceiveError, TangleEventReceiver},
logging,
@@ -20,9 +21,7 @@ use std::{
sync::atomic::{AtomicU64, Ordering},
time::{Instant, SystemTime, UNIX_EPOCH},
};
-use tangle_protocol::{
- ClientMessage, Filter, RelayMessage, SubscriptionId, UnixTimestamp, parse_client_message,
-};
+use tangle_protocol::{ClientMessage, Filter, RelayMessage, SubscriptionId, UnixTimestamp};
use tokio::sync::{mpsc, watch};
#[derive(Debug)]
@@ -248,7 +247,7 @@ impl TangleWebSocketSession {
.map(|_| TangleSessionControl::Continue)
.unwrap_or_else(|control| control);
}
- let replies = match parse_client_message(raw) {
+ let replies = match parse_runtime_client_message(raw) {
Ok(message) => match self.handle_client_message(message).await {
Ok(replies) => replies,
Err(error) => vec![RelayMessage::Notice(error.prefixed_message())],