commit ef3c6d9b2f56159c31a3e78386674fc32c46b97a
parent d02d3229a04235379fa05322ba514782bd42d5ce
Author: triesap <tyson@radroots.org>
Date: Sat, 6 Jun 2026 01:19:17 -0700
ws: handle auth messages
Diffstat:
1 file changed, 126 insertions(+), 7 deletions(-)
diff --git a/crates/tangle_runtime/src/lib.rs b/crates/tangle_runtime/src/lib.rs
@@ -15,7 +15,7 @@ use tangle_core::{
MarketplaceListingStatus, MarketplaceQuery, MarketplaceQueryError, MarketplaceQuerySpec,
MarketplaceSort, RateLimitConfig, RuntimeLimits, SubscriptionManager, SubscriptionMatcher,
};
-use tangle_nips::{FulfillmentMethod, ListingUnit};
+use tangle_nips::{FulfillmentMethod, ListingUnit, parse_relay_auth_event};
use tangle_protocol::{
ClientMessage, Event, EventId, PublicKeyHex, RelayMessage, UnixTimestamp, parse_client_message,
};
@@ -325,6 +325,46 @@ impl EventMessageHandler {
}
}
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+pub struct AuthMessageHandler;
+
+impl AuthMessageHandler {
+ pub fn issue_challenge(
+ &self,
+ connection: &mut RelayConnection,
+ challenge: &str,
+ issued_at: UnixTimestamp,
+ ) -> RelayMessage {
+ match connection.auth_mut().issue_challenge(challenge, issued_at) {
+ Ok(challenge) => RelayMessage::Auth(challenge.value),
+ Err(error) => RelayMessage::Notice(format!("error: {error}")),
+ }
+ }
+
+ pub fn handle_auth(
+ &self,
+ connection: &mut RelayConnection,
+ event: Event,
+ now: UnixTimestamp,
+ ) -> RelayMessage {
+ let event_id = event.id().clone();
+ let auth = match parse_relay_auth_event(&event) {
+ Ok(Some(auth)) => auth,
+ Ok(None) => {
+ return ok_rejected(
+ event_id,
+ "invalid: AUTH message must contain kind 22242".to_owned(),
+ );
+ }
+ Err(error) => return ok_rejected(event_id, format!("invalid: {error}")),
+ };
+ match connection.auth_mut().authenticate(&auth, now) {
+ Ok(_) => ok_accepted(event_id),
+ Err(error) => ok_rejected(event_id, format!("auth-required: {error}")),
+ }
+ }
+}
+
fn admission_context(connection: &RelayConnection) -> AdmissionContext {
connection
.auth()
@@ -1510,12 +1550,12 @@ fn invalid_parameter(field: &'static str, requirement: &str) -> ApiError {
#[cfg(test)]
mod tests {
use super::{
- ApiError, ApiErrorBody, ApiErrorCode, ApiErrorEnvelope, ClientFrame, ClientFrameOutcome,
- ClientMessageLoop, EventMessageHandler, ListingsHttpState, ReadinessCheckStatus,
- ReadinessState, RelayConnection, RelayConnectionConfig, RelayConnectionId,
- RelayInfoDocument, TANGLE_RELAY_SOFTWARE, TANGLE_SUPPORTED_NIPS, WebSocketHttpState,
- health_router, listing_item_document, listing_projection_query, listings_router,
- parse_listing_query, parse_marketplace_search_query, relay_info_router,
+ ApiError, ApiErrorBody, ApiErrorCode, ApiErrorEnvelope, AuthMessageHandler, ClientFrame,
+ ClientFrameOutcome, ClientMessageLoop, EventMessageHandler, ListingsHttpState,
+ ReadinessCheckStatus, ReadinessState, RelayConnection, RelayConnectionConfig,
+ RelayConnectionId, RelayInfoDocument, TANGLE_RELAY_SOFTWARE, TANGLE_SUPPORTED_NIPS,
+ WebSocketHttpState, health_router, listing_item_document, listing_projection_query,
+ listings_router, parse_listing_query, parse_marketplace_search_query, relay_info_router,
search_document_query, websocket_router,
};
use axum::{body::Body, response::IntoResponse};
@@ -1913,6 +1953,85 @@ mod tests {
}
}
+ #[test]
+ fn auth_message_handler_issues_and_accepts_auth_events() {
+ let handler = AuthMessageHandler;
+ let mut connection = RelayConnection::new(
+ RelayConnectionId::new("auth").expect("connection id"),
+ RelayConnectionConfig::default(),
+ );
+ let auth = build_fixture_event(&auth_event_spec()).expect("auth");
+
+ assert_eq!(
+ handler.issue_challenge(
+ &mut connection,
+ " challenge-001 ",
+ UnixTimestamp::new(1_714_124_430)
+ ),
+ RelayMessage::Auth("challenge-001".to_owned())
+ );
+ assert_eq!(
+ handler.handle_auth(
+ &mut connection,
+ auth.clone(),
+ UnixTimestamp::new(1_714_124_435)
+ ),
+ RelayMessage::Ok {
+ event_id: auth.id().clone(),
+ accepted: true,
+ message: String::new()
+ }
+ );
+ assert_eq!(
+ connection.auth().authenticated_pubkey(),
+ Some(auth.unsigned().pubkey())
+ );
+ assert_eq!(
+ handler.issue_challenge(&mut connection, " ", UnixTimestamp::new(1_714_124_436)),
+ RelayMessage::Notice("error: auth challenge must not be empty".to_owned())
+ );
+ }
+
+ #[test]
+ fn auth_message_handler_rejects_invalid_auth_messages() {
+ let handler = AuthMessageHandler;
+ let listing = build_fixture_event(&valid_public_listing_spec()).expect("listing");
+ let auth = build_fixture_event(&auth_event_spec()).expect("auth");
+ let mut missing_challenge = RelayConnection::new(
+ RelayConnectionId::new("missing-challenge").expect("connection id"),
+ RelayConnectionConfig::default(),
+ );
+ let mut wrong_kind = RelayConnection::new(
+ RelayConnectionId::new("wrong-kind").expect("connection id"),
+ RelayConnectionConfig::default(),
+ );
+
+ match handler.handle_auth(
+ &mut missing_challenge,
+ auth.clone(),
+ UnixTimestamp::new(1_714_124_435),
+ ) {
+ RelayMessage::Ok {
+ accepted: false,
+ message,
+ ..
+ } => assert_eq!(message, "auth-required: auth challenge is missing"),
+ outcome => panic!("unexpected outcome: {outcome:?}"),
+ }
+ match handler.handle_auth(
+ &mut wrong_kind,
+ listing.clone(),
+ UnixTimestamp::new(1_714_124_435),
+ ) {
+ RelayMessage::Ok {
+ accepted: false,
+ message,
+ ..
+ } => assert_eq!(message, "invalid: AUTH message must contain kind 22242"),
+ outcome => panic!("unexpected outcome: {outcome:?}"),
+ }
+ }
+
#[tokio::test]
async fn api_error_into_response_keeps_public_envelope_shape() {
let response = ApiError::not_found("listing not found").into_response();