1pub(crate) mod announcement_manager;
8pub mod correlation_store;
9pub mod session_store;
10
11pub use correlation_store::{RouteEntry, ServerEventRouteStore};
12pub use session_store::{SessionSnapshot, SessionStore};
13use tokio::sync::Mutex as AsyncMutex;
14use tokio::sync::RwLock;
15
16use std::collections::HashMap;
17use std::num::NonZeroUsize;
18use std::sync::atomic::{AtomicU64, Ordering};
19use std::sync::{Arc, Mutex};
20use std::time::{Duration, Instant};
21
22use lru::LruCache;
23use nostr_sdk::prelude::*;
24use tokio_util::sync::CancellationToken;
25
26use crate::core::constants::*;
27use crate::core::error::{Error, Result};
28use crate::core::types::*;
29use crate::core::validation;
30use crate::encryption;
31use crate::relay::{RelayPool, RelayPoolTrait};
32use crate::transport::base::BaseTransport;
33use crate::transport::discovery_tags::learn_peer_capabilities;
34use crate::transport::open_stream::{
35 open_stream_frame_from_notification, FrameOutcome, KeepaliveAction, OnAbortHook, OnCloseHook,
36 OpenStreamConfig, OpenStreamFrame, OpenStreamReceiver, OpenStreamRegistryPolicy,
37 OpenStreamWriter, OpenStreamWriterOptions, PublishFrame,
38};
39use crate::transport::oversized_transfer::{
40 build_oversized_frames, progress_token_string, resolve_safe_chunk_size, OversizedFrame,
41 OversizedSenderOptions, OversizedTransferConfig, OversizedTransferReceiver, TransferPolicy,
42 ACCEPT_PROGRESS,
43};
44
45const LOG_TARGET: &str = "contextvm_sdk::transport::server";
46
47fn oversized_support_tags(config: &NostrServerTransportConfig) -> Vec<Tag> {
50 if config.oversized_transfer.enabled {
51 vec![Tag::custom(
52 TagKind::Custom(tags::SUPPORT_OVERSIZED_TRANSFER.into()),
53 Vec::<String>::new(),
54 )]
55 } else {
56 Vec::new()
57 }
58}
59
60fn open_stream_support_tags(config: &OpenStreamConfig) -> Vec<Tag> {
63 if config.enabled {
64 vec![Tag::custom(
65 TagKind::Custom(tags::SUPPORT_OPEN_STREAM.into()),
66 Vec::<String>::new(),
67 )]
68 } else {
69 Vec::new()
70 }
71}
72
73fn internal_common_capability_tags(config: &NostrServerTransportConfig) -> Vec<Tag> {
76 let mut tags = oversized_support_tags(config);
77 tags.extend(open_stream_support_tags(&config.open_stream));
78 tags
79}
80
81fn new_oversized_receiver_store(
85 max_sessions: usize,
86) -> Arc<RwLock<LruCache<String, OversizedTransferReceiver>>> {
87 Arc::new(RwLock::new(LruCache::new(
88 NonZeroUsize::new(max_sessions).unwrap_or(NonZeroUsize::new(1).unwrap()),
89 )))
90}
91
92fn new_open_stream_receiver_store(
102 max_sessions: usize,
103) -> Arc<AsyncMutex<LruCache<String, OpenStreamReceiver>>> {
104 Arc::new(AsyncMutex::new(LruCache::new(
105 NonZeroUsize::new(max_sessions).unwrap_or(NonZeroUsize::new(1).unwrap()),
106 )))
107}
108
109#[derive(Clone)]
118struct RouteSnapshot {
119 client_pubkey: PublicKey,
120 original_request_id: serde_json::Value,
121 is_encrypted: bool,
122 mirrored_wrap_kind: Option<u16>,
123}
124
125struct OpenStreamSlot {
133 writer: OpenStreamWriter,
134 snapshot: RouteSnapshot,
135 pending_response: Option<JsonRpcMessage>,
138 terminated: bool,
141}
142
143#[derive(Clone)]
146struct ServerOpenStreamState {
147 enabled: bool,
149 policy: OpenStreamRegistryPolicy,
151 receiver: Arc<AsyncMutex<LruCache<String, OpenStreamReceiver>>>,
153 slots: Arc<Mutex<HashMap<String, OpenStreamSlot>>>,
155 token_to_event: Arc<Mutex<HashMap<String, String>>>,
158 control_progress: Arc<AtomicU64>,
162}
163
164impl ServerOpenStreamState {
165 fn new(config: &OpenStreamConfig, max_sessions: usize) -> Self {
166 Self {
167 enabled: config.enabled,
168 policy: config.into(),
169 receiver: new_open_stream_receiver_store(max_sessions),
170 slots: Arc::new(Mutex::new(HashMap::new())),
171 token_to_event: Arc::new(Mutex::new(HashMap::new())),
172 control_progress: Arc::new(AtomicU64::new(0)),
173 }
174 }
175
176 fn next_control_progress(&self) -> u64 {
178 self.control_progress.fetch_add(1, Ordering::SeqCst) + 1
179 }
180
181 fn lock_slots(&self) -> std::sync::MutexGuard<'_, HashMap<String, OpenStreamSlot>> {
183 match self.slots.lock() {
184 Ok(g) => g,
185 Err(p) => p.into_inner(),
186 }
187 }
188
189 fn lock_token_index(&self) -> std::sync::MutexGuard<'_, HashMap<String, String>> {
190 match self.token_to_event.lock() {
191 Ok(g) => g,
192 Err(p) => p.into_inner(),
193 }
194 }
195
196 fn writer_for(&self, event_id: &str) -> Option<OpenStreamWriter> {
199 self.lock_slots().get(event_id).map(|s| s.writer.clone())
200 }
201
202 fn event_id_for_token(&self, token: &str) -> Option<String> {
204 self.lock_token_index().get(token).cloned()
205 }
206}
207
208enum OpenStreamDeferral {
210 Deferred,
212 SendNow {
214 snapshot: RouteSnapshot,
215 response: JsonRpcMessage,
216 },
217 Passthrough(JsonRpcMessage),
219}
220
221#[derive(Debug, Clone)]
223#[non_exhaustive]
224pub struct NostrServerTransportConfig {
225 pub relay_urls: Vec<String>,
227 pub encryption_mode: EncryptionMode,
229 pub gift_wrap_mode: GiftWrapMode,
231 pub server_info: Option<ServerInfo>,
233 pub is_announced_server: bool,
235 pub allowed_public_keys: Vec<String>,
237 pub excluded_capabilities: Vec<CapabilityExclusion>,
239 pub max_sessions: usize,
241 pub cleanup_interval: Duration,
243 pub session_timeout: Duration,
245 pub request_timeout: Duration,
251 pub relay_list_urls: Option<Vec<String>>,
255 pub bootstrap_relay_urls: Option<Vec<String>>,
260 pub publish_relay_list: bool,
262 pub profile_metadata: Option<ProfileMetadata>,
264 pub oversized_transfer: OversizedTransferConfig,
266 pub open_stream: OpenStreamConfig,
272}
273
274impl Default for NostrServerTransportConfig {
275 fn default() -> Self {
276 Self {
277 relay_urls: vec!["wss://relay.damus.io".to_string()],
278 encryption_mode: EncryptionMode::Optional,
279 gift_wrap_mode: GiftWrapMode::Optional,
280 server_info: None,
281 is_announced_server: false,
282 allowed_public_keys: Vec::new(),
283 excluded_capabilities: Vec::new(),
284 max_sessions: session_store::DEFAULT_MAX_SESSIONS,
285 cleanup_interval: Duration::from_secs(60),
286 session_timeout: Duration::from_secs(300),
287 request_timeout: Duration::from_secs(60),
288 relay_list_urls: None,
289 bootstrap_relay_urls: None,
290 publish_relay_list: true,
291 profile_metadata: None,
292 oversized_transfer: OversizedTransferConfig::default(),
293 open_stream: OpenStreamConfig::default(),
294 }
295 }
296}
297
298pub struct NostrServerTransport {
300 base: BaseTransport,
302 config: NostrServerTransportConfig,
304 announcement_manager: announcement_manager::AnnouncementManager,
306 sessions: SessionStore,
308 event_routes: ServerEventRouteStore,
310 request_wrap_kinds: Arc<RwLock<HashMap<String, Option<u16>>>>,
312 seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
316 oversized_receiver: Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
321 open_stream: ServerOpenStreamState,
325 message_tx: Option<tokio::sync::mpsc::UnboundedSender<IncomingRequest>>,
327 message_rx: Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>>,
328 cancellation_token: CancellationToken,
330 task_handles: Vec<tokio::task::JoinHandle<()>>,
332}
333
334impl NostrServerTransportConfig {
335 pub fn with_encryption_mode(mut self, mode: EncryptionMode) -> Self {
337 self.encryption_mode = mode;
338 self
339 }
340 pub fn with_gift_wrap_mode(mut self, mode: GiftWrapMode) -> Self {
342 self.gift_wrap_mode = mode;
343 self
344 }
345 pub fn with_server_info(mut self, info: ServerInfo) -> Self {
347 self.server_info = Some(info);
348 self
349 }
350 pub fn with_announced_server(mut self, announced: bool) -> Self {
352 self.is_announced_server = announced;
353 self
354 }
355 pub fn with_allowed_public_keys(mut self, keys: Vec<String>) -> Self {
357 self.allowed_public_keys = keys;
358 self
359 }
360 pub fn with_excluded_capabilities(mut self, caps: Vec<CapabilityExclusion>) -> Self {
362 self.excluded_capabilities = caps;
363 self
364 }
365 pub fn with_max_sessions(mut self, max: usize) -> Self {
367 self.max_sessions = max;
368 self
369 }
370 pub fn with_relay_urls(mut self, urls: Vec<String>) -> Self {
372 self.relay_urls = urls;
373 self
374 }
375 pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
377 self.cleanup_interval = interval;
378 self
379 }
380 pub fn with_session_timeout(mut self, timeout: Duration) -> Self {
382 self.session_timeout = timeout;
383 self
384 }
385 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
387 self.request_timeout = timeout;
388 self
389 }
390 pub fn with_relay_list_urls(mut self, urls: Vec<String>) -> Self {
392 self.relay_list_urls = Some(urls);
393 self
394 }
395 pub fn with_bootstrap_relay_urls(mut self, urls: Vec<String>) -> Self {
397 self.bootstrap_relay_urls = Some(urls);
398 self
399 }
400 pub fn with_publish_relay_list(mut self, publish: bool) -> Self {
402 self.publish_relay_list = publish;
403 self
404 }
405 pub fn with_profile_metadata(mut self, metadata: ProfileMetadata) -> Self {
407 self.profile_metadata = Some(metadata);
408 self
409 }
410 pub fn with_oversized_transfer(mut self, config: OversizedTransferConfig) -> Self {
412 self.oversized_transfer = config;
413 self
414 }
415 pub fn with_oversized_enabled(mut self, enabled: bool) -> Self {
417 self.oversized_transfer.enabled = enabled;
418 self
419 }
420 pub fn with_open_stream(mut self, config: OpenStreamConfig) -> Self {
423 self.open_stream = config;
424 self
425 }
426}
427
428#[derive(Debug)]
430#[non_exhaustive]
431pub struct IncomingRequest {
432 pub message: JsonRpcMessage,
434 pub client_pubkey: String,
436 pub event_id: String,
438 pub is_encrypted: bool,
440 pub event: Option<Event>,
450}
451
452#[derive(Debug, Clone)]
473pub struct ClientPubkey(pub String);
474
475#[derive(Debug, Clone)]
508pub struct InboundEvent(pub Event);
509
510impl NostrServerTransport {
511 pub async fn new<T>(signer: T, config: NostrServerTransportConfig) -> Result<Self>
513 where
514 T: IntoNostrSigner,
515 {
516 let relay_pool: Arc<dyn RelayPoolTrait> =
517 Arc::new(RelayPool::new(signer).await.map_err(|error| {
518 tracing::error!(
519 target: LOG_TARGET,
520 error = %error,
521 "Failed to initialize relay pool for server transport"
522 );
523 error
524 })?);
525 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
526 let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new(
527 NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
528 )));
529
530 tracing::info!(
531 target: LOG_TARGET,
532 relay_count = config.relay_urls.len(),
533 announced = config.is_announced_server,
534 encryption_mode = ?config.encryption_mode,
535 gift_wrap_mode = ?config.gift_wrap_mode,
536 "Created server transport"
537 );
538 let mut announcement_manager = announcement_manager::AnnouncementManager::new(
539 Arc::clone(&relay_pool),
540 config.server_info.clone(),
541 config.encryption_mode,
542 config.gift_wrap_mode,
543 tx.clone(),
544 config.relay_urls.clone(),
545 config.relay_list_urls.clone(),
546 config.bootstrap_relay_urls.clone(),
547 config.publish_relay_list,
548 config.profile_metadata.clone(),
549 );
550 announcement_manager.set_internal_common_tags(internal_common_capability_tags(&config));
553 Ok(Self {
554 announcement_manager,
555 base: BaseTransport {
556 relay_pool,
557 encryption_mode: config.encryption_mode,
558 is_connected: false,
559 },
560 sessions: SessionStore::with_capacity(config.max_sessions),
561 oversized_receiver: new_oversized_receiver_store(config.max_sessions),
562 open_stream: ServerOpenStreamState::new(&config.open_stream, config.max_sessions),
563 config,
564 event_routes: ServerEventRouteStore::new(),
565 request_wrap_kinds: Arc::new(RwLock::new(HashMap::new())),
566 seen_gift_wrap_ids,
567 message_tx: Some(tx),
568 message_rx: Some(rx),
569 cancellation_token: CancellationToken::new(),
570 task_handles: Vec::new(),
571 })
572 }
573
574 pub async fn with_relay_pool(
576 config: NostrServerTransportConfig,
577 relay_pool: Arc<dyn RelayPoolTrait>,
578 ) -> Result<Self> {
579 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
580 let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new(
581 NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
582 )));
583
584 tracing::info!(
585 target: LOG_TARGET,
586 relay_count = config.relay_urls.len(),
587 announced = config.is_announced_server,
588 encryption_mode = ?config.encryption_mode,
589 "Created server transport (with_relay_pool)"
590 );
591 let mut announcement_manager = announcement_manager::AnnouncementManager::new(
592 Arc::clone(&relay_pool),
593 config.server_info.clone(),
594 config.encryption_mode,
595 config.gift_wrap_mode,
596 tx.clone(),
597 config.relay_urls.clone(),
598 config.relay_list_urls.clone(),
599 config.bootstrap_relay_urls.clone(),
600 config.publish_relay_list,
601 config.profile_metadata.clone(),
602 );
603 announcement_manager.set_internal_common_tags(internal_common_capability_tags(&config));
606 Ok(Self {
607 announcement_manager,
608 base: BaseTransport {
609 relay_pool,
610 encryption_mode: config.encryption_mode,
611 is_connected: false,
612 },
613 sessions: SessionStore::with_capacity(config.max_sessions),
614 oversized_receiver: new_oversized_receiver_store(config.max_sessions),
615 open_stream: ServerOpenStreamState::new(&config.open_stream, config.max_sessions),
616 config,
617 request_wrap_kinds: Arc::new(RwLock::new(HashMap::new())),
618 event_routes: ServerEventRouteStore::new(),
619 seen_gift_wrap_ids,
620 message_tx: Some(tx),
621 message_rx: Some(rx),
622 cancellation_token: CancellationToken::new(),
623 task_handles: Vec::new(),
624 })
625 }
626
627 pub async fn start(&mut self) -> Result<()> {
629 self.base
630 .connect(&self.config.relay_urls)
631 .await
632 .map_err(|error| {
633 tracing::error!(
634 target: LOG_TARGET,
635 error = %error,
636 "Failed to connect server transport to relays"
637 );
638 error
639 })?;
640
641 let pubkey = self.base.get_public_key().await.map_err(|error| {
642 tracing::error!(
643 target: LOG_TARGET,
644 error = %error,
645 "Failed to fetch server transport public key"
646 );
647 error
648 })?;
649 tracing::info!(
650 target: LOG_TARGET,
651 pubkey = %pubkey.to_hex(),
652 "Server transport started"
653 );
654
655 self.base
656 .subscribe_for_pubkey(&pubkey)
657 .await
658 .map_err(|error| {
659 tracing::error!(
660 target: LOG_TARGET,
661 error = %error,
662 pubkey = %pubkey.to_hex(),
663 "Failed to subscribe server transport for pubkey"
664 );
665 error
666 })?;
667
668 let relay_pool = Arc::clone(&self.base.relay_pool);
670 let sessions = self.sessions.clone();
671 let event_routes = self.event_routes.clone();
672 let request_wrap_kinds = self.request_wrap_kinds.clone();
673 let tx = self
674 .message_tx
675 .as_ref()
676 .expect("message_tx must exist before start()")
677 .clone();
678 let allowed = self.config.allowed_public_keys.clone();
679 let excluded = self.config.excluded_capabilities.clone();
680 let encryption_mode = self.config.encryption_mode;
681 let gift_wrap_mode = self.config.gift_wrap_mode;
682 let is_announced_server = self.config.is_announced_server;
683 let oversized_enabled = self.config.oversized_transfer.enabled;
684 let oversized_receiver = self.oversized_receiver.clone();
685 let transfer_policy: TransferPolicy = (&self.config.oversized_transfer).into();
686 let common_tags_snapshot = self.announcement_manager.common_tags_snapshot();
687 let seen_gift_wrap_ids = self.seen_gift_wrap_ids.clone();
688 let open_stream = self.open_stream.clone();
689 let event_loop_token = self.cancellation_token.child_token();
690
691 let event_loop_handle = tokio::spawn(async move {
692 Self::event_loop(
693 relay_pool,
694 sessions,
695 event_routes,
696 request_wrap_kinds,
697 tx,
698 allowed,
699 excluded,
700 encryption_mode,
701 gift_wrap_mode,
702 is_announced_server,
703 oversized_enabled,
704 oversized_receiver,
705 transfer_policy,
706 common_tags_snapshot,
707 seen_gift_wrap_ids,
708 open_stream,
709 event_loop_token,
710 )
711 .await;
712 });
713
714 let sessions_cleanup = self.sessions.clone();
716 let event_routes_cleanup = self.event_routes.clone();
717 let request_wrap_kinds_cleanup = self.request_wrap_kinds.clone();
718 let cleanup_interval = self.config.cleanup_interval;
719 let session_timeout = self.config.session_timeout;
720 let request_timeout = self.config.request_timeout;
721 let cleanup_token = self.cancellation_token.child_token();
722
723 let cleanup_handle = tokio::spawn(async move {
724 let mut interval = tokio::time::interval(cleanup_interval);
725 loop {
726 tokio::select! {
727 _ = cleanup_token.cancelled() => {
728 tracing::info!(
729 target: LOG_TARGET,
730 "Server cleanup task cancelled"
731 );
732 break;
733 }
734 _ = interval.tick() => {
735 let cleaned = Self::cleanup_sessions(
736 &sessions_cleanup,
737 &event_routes_cleanup,
738 &request_wrap_kinds_cleanup,
739 session_timeout,
740 )
741 .await;
742 if cleaned > 0 {
743 tracing::info!(
744 target: LOG_TARGET,
745 cleaned_sessions = cleaned,
746 "Cleaned up inactive sessions"
747 );
748 }
749 }
750 }
751
752 let swept_event_ids = event_routes_cleanup
754 .sweep_stale_routes(request_timeout)
755 .await;
756 if !swept_event_ids.is_empty() {
757 let mut kinds_w = request_wrap_kinds_cleanup.write().await;
758 for event_id in &swept_event_ids {
759 kinds_w.remove(event_id);
760 }
761 drop(kinds_w);
762 tracing::warn!(
763 target: LOG_TARGET,
764 swept = swept_event_ids.len(),
765 timeout_secs = request_timeout.as_secs(),
766 "Swept stale event routes (rmcp handles timeout errors)"
767 );
768 }
769 }
770 });
771
772 self.task_handles.push(event_loop_handle);
773 self.task_handles.push(cleanup_handle);
774
775 tracing::info!(
776 target: LOG_TARGET,
777 relay_count = self.config.relay_urls.len(),
778 cleanup_interval_secs = self.config.cleanup_interval.as_secs(),
779 session_timeout_secs = self.config.session_timeout.as_secs(),
780 "Server transport loops spawned"
781 );
782 Ok(())
783 }
784
785 pub async fn close(&mut self) -> Result<()> {
787 self.cancellation_token.cancel();
788 for handle in self.task_handles.drain(..) {
789 let _ = handle.await;
790 }
791 self.announcement_manager.shutdown();
792 self.message_tx.take();
793 self.base.disconnect().await?;
794 self.sessions.clear().await;
795 self.event_routes.clear().await;
796 self.oversized_receiver.write().await.clear();
797 {
800 let mut receivers = self.open_stream.receiver.lock().await;
801 for (_, receiver) in receivers.iter_mut() {
802 receiver.clear();
803 }
804 receivers.clear();
805 }
806 for (_, slot) in self.open_stream.lock_slots().drain() {
807 slot.writer.dispose();
808 }
809 self.open_stream.lock_token_index().clear();
810 Ok(())
811 }
812
813 pub async fn send_response(&self, event_id: &str, mut response: JsonRpcMessage) -> Result<()> {
815 if self.open_stream.enabled {
819 match self.try_defer_open_stream_response(event_id, response) {
820 OpenStreamDeferral::Deferred => return Ok(()),
822 OpenStreamDeferral::SendNow { snapshot, response } => {
824 return self
825 .send_open_stream_deferred_response(event_id, &snapshot, response)
826 .await;
827 }
828 OpenStreamDeferral::Passthrough(returned) => response = returned,
830 }
831 }
832
833 let route = self.event_routes.pop(event_id).await.ok_or_else(|| {
836 tracing::error!(
837 target: LOG_TARGET,
838 event_id = %event_id,
839 "No client found for response correlation"
840 );
841 Error::Other(format!("No client found for event {event_id}"))
842 })?;
843
844 let client_pubkey_hex = route.client_pubkey;
845 let original_request_id = route.original_request_id;
846 let progress_token = route.progress_token;
847
848 let mut sessions_w = self.sessions.write().await;
849 let session = sessions_w.get_mut(&client_pubkey_hex).ok_or_else(|| {
850 tracing::error!(
851 target: LOG_TARGET,
852 client_pubkey = %client_pubkey_hex,
853 "No session for correlated client"
854 );
855 Error::Other(format!("No session for client {client_pubkey_hex}"))
856 })?;
857
858 match &mut response {
860 JsonRpcMessage::Response(r) => r.id = original_request_id.clone(),
861 JsonRpcMessage::ErrorResponse(r) => r.id = original_request_id.clone(),
862 _ => {}
863 }
864
865 let serialized = serde_json::to_string(&response)?;
869
870 let is_encrypted = session.is_encrypted;
871 let supports_oversized_transfer = session.supports_oversized_transfer;
873
874 let discovery_tags = self.take_pending_server_discovery_tags(session);
876 drop(sessions_w);
877
878 let mirrored_wrap_kind = self
880 .request_wrap_kinds
881 .read()
882 .await
883 .get(event_id)
884 .copied()
885 .flatten();
886
887 let client_pubkey = PublicKey::from_hex(&client_pubkey_hex).map_err(|error| {
888 tracing::error!(
889 target: LOG_TARGET,
890 error = %error,
891 client_pubkey = %client_pubkey_hex,
892 "Invalid client pubkey in session map"
893 );
894 Error::Other(error.to_string())
895 })?;
896
897 let event_id_parsed = EventId::from_hex(event_id).map_err(|error| {
898 tracing::error!(
899 target: LOG_TARGET,
900 error = %error,
901 event_id = %event_id,
902 "Invalid event id while sending response"
903 );
904 Error::Other(error.to_string())
905 })?;
906
907 let base_tags = BaseTransport::create_response_tags(&client_pubkey, &event_id_parsed);
908 let tags = BaseTransport::compose_outbound_tags(&base_tags, &discovery_tags, &[]);
909 let gift_wrap_kind = Self::select_outbound_gift_wrap_kind(
910 self.config.gift_wrap_mode,
911 is_encrypted,
912 mirrored_wrap_kind,
913 );
914
915 let oversized_eligible = self.config.oversized_transfer.enabled
919 && progress_token.is_some()
920 && supports_oversized_transfer;
921 let threshold = self.config.oversized_transfer.threshold;
922
923 let mut reuse_event: Option<Event> = None;
931 let fragment = if !oversized_eligible {
932 false
933 } else if serialized.len() >= threshold {
934 true
935 } else {
936 match self
937 .base
938 .prepare_mcp_message(
939 &response,
940 &client_pubkey,
941 CTXVM_MESSAGES_KIND,
942 tags.clone(),
943 Some(is_encrypted),
944 gift_wrap_kind,
945 )
946 .await
947 {
948 Ok((_id, publishable)) => {
949 let published_len = serde_json::to_string(&publishable)
950 .map(|s| s.len())
951 .unwrap_or(usize::MAX);
952 if published_len > threshold {
953 true
954 } else {
955 reuse_event = Some(publishable);
956 false
957 }
958 }
959 Err(error) => {
962 tracing::debug!(
963 target: LOG_TARGET,
964 error = %error,
965 event_id = %event_id,
966 "Single-event build failed; sending response as oversized transfer"
967 );
968 true
969 }
970 }
971 };
972
973 let send_result: Result<()> = if fragment {
976 self.send_oversized_response(
977 &serialized,
978 progress_token.as_deref().unwrap_or_default(),
979 &client_pubkey,
980 &base_tags,
981 tags,
982 is_encrypted,
983 gift_wrap_kind,
984 )
985 .await
986 } else if let Some(publishable) = reuse_event {
987 self.base
989 .relay_pool
990 .publish_event(&publishable)
991 .await
992 .map(|_| ())
993 } else {
994 self.base
995 .send_mcp_message(
996 &response,
997 &client_pubkey,
998 CTXVM_MESSAGES_KIND,
999 tags,
1000 Some(is_encrypted),
1001 gift_wrap_kind,
1002 )
1003 .await
1004 .map(|_| ())
1005 };
1006
1007 if let Err(error) = send_result {
1008 tracing::error!(
1009 target: LOG_TARGET,
1010 error = %error,
1011 client_pubkey = %client_pubkey_hex,
1012 event_id = %event_id,
1013 "Failed to publish response message"
1014 );
1015
1016 self.event_routes
1018 .register(
1019 event_id.to_string(),
1020 client_pubkey_hex,
1021 original_request_id,
1022 progress_token,
1023 )
1024 .await;
1025
1026 return Err(error);
1027 }
1028
1029 self.request_wrap_kinds.write().await.remove(event_id);
1031
1032 let mut sessions = self.sessions.write().await;
1033 if let Some(session) = sessions.get_mut(&client_pubkey_hex) {
1034 if let Some(token) = progress_token {
1036 session.pending_requests.remove(&token);
1037 }
1038 session.event_to_progress_token.remove(event_id);
1039 session.pending_requests.remove(event_id);
1040 }
1041 drop(sessions);
1042
1043 tracing::debug!(
1044 target: LOG_TARGET,
1045 client_pubkey = %client_pubkey_hex,
1046 event_id = %event_id,
1047 encrypted = is_encrypted,
1048 "Sent server response and cleaned correlation state"
1049 );
1050 Ok(())
1051 }
1052
1053 #[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
1057 pub(crate) fn get_open_stream_writer(&self, event_id: &str) -> Option<OpenStreamWriter> {
1058 if !self.open_stream.enabled {
1059 return None;
1060 }
1061 self.open_stream.writer_for(event_id)
1062 }
1063
1064 fn try_defer_open_stream_response(
1068 &self,
1069 event_id: &str,
1070 response: JsonRpcMessage,
1071 ) -> OpenStreamDeferral {
1072 let mut slots = self.open_stream.lock_slots();
1073 let Some(slot) = slots.get_mut(event_id) else {
1074 return OpenStreamDeferral::Passthrough(response);
1075 };
1076
1077 if !slot.writer.has_started() {
1078 let token = slot.writer.progress_token().to_string();
1082 slots.remove(event_id);
1083 drop(slots);
1084 self.open_stream.lock_token_index().remove(&token);
1085 return OpenStreamDeferral::Passthrough(response);
1086 }
1087
1088 if slot.terminated {
1089 let snapshot = slot.snapshot.clone();
1092 let token = slot.writer.progress_token().to_string();
1093 slots.remove(event_id);
1094 drop(slots);
1095 self.open_stream.lock_token_index().remove(&token);
1096 OpenStreamDeferral::SendNow { snapshot, response }
1097 } else {
1098 slot.pending_response = Some(response);
1101 OpenStreamDeferral::Deferred
1102 }
1103 }
1104
1105 async fn send_open_stream_deferred_response(
1108 &self,
1109 event_id: &str,
1110 snapshot: &RouteSnapshot,
1111 response: JsonRpcMessage,
1112 ) -> Result<()> {
1113 Self::publish_open_stream_deferred_response(
1114 &self.base,
1115 self.config.gift_wrap_mode,
1116 event_id,
1117 snapshot,
1118 response,
1119 )
1120 .await
1121 }
1122
1123 async fn publish_open_stream_deferred_response(
1126 base: &BaseTransport,
1127 gift_wrap_mode: GiftWrapMode,
1128 event_id: &str,
1129 snapshot: &RouteSnapshot,
1130 mut response: JsonRpcMessage,
1131 ) -> Result<()> {
1132 match &mut response {
1135 JsonRpcMessage::Response(r) => r.id = snapshot.original_request_id.clone(),
1136 JsonRpcMessage::ErrorResponse(r) => r.id = snapshot.original_request_id.clone(),
1137 _ => {}
1138 }
1139 let event_id_parsed = EventId::from_hex(event_id).map_err(|error| {
1140 Error::Other(format!("Invalid event id for deferred response: {error}"))
1141 })?;
1142 let tags = BaseTransport::create_response_tags(&snapshot.client_pubkey, &event_id_parsed);
1145 let gift_wrap_kind = Self::select_outbound_gift_wrap_kind(
1146 gift_wrap_mode,
1147 snapshot.is_encrypted,
1148 snapshot.mirrored_wrap_kind,
1149 );
1150 base.send_mcp_message(
1151 &response,
1152 &snapshot.client_pubkey,
1153 CTXVM_MESSAGES_KIND,
1154 tags,
1155 Some(snapshot.is_encrypted),
1156 gift_wrap_kind,
1157 )
1158 .await
1159 .map(|_| ())
1160 }
1161
1162 async fn flush_open_stream_response(
1166 state: &ServerOpenStreamState,
1167 base: &BaseTransport,
1168 gift_wrap_mode: GiftWrapMode,
1169 event_id: &str,
1170 ) {
1171 let ready = {
1172 let mut slots = state.lock_slots();
1173 match slots.get_mut(event_id) {
1174 Some(slot) => {
1175 slot.terminated = true;
1176 slot.pending_response.take().map(|response| {
1177 (
1178 slot.snapshot.clone(),
1179 slot.writer.progress_token().to_string(),
1180 response,
1181 )
1182 })
1183 }
1184 None => None,
1185 }
1186 };
1187
1188 let Some((snapshot, token, response)) = ready else {
1189 return;
1192 };
1193
1194 state.lock_slots().remove(event_id);
1197 state.lock_token_index().remove(&token);
1198 if let Err(error) = Self::publish_open_stream_deferred_response(
1199 base,
1200 gift_wrap_mode,
1201 event_id,
1202 &snapshot,
1203 response,
1204 )
1205 .await
1206 {
1207 tracing::error!(
1208 target: LOG_TARGET,
1209 error = %error,
1210 event_id = %event_id,
1211 "Failed to flush deferred open-stream response"
1212 );
1213 }
1214 }
1215
1216 #[allow(clippy::too_many_arguments)]
1226 async fn send_oversized_response(
1227 &self,
1228 serialized: &str,
1229 progress_token: &str,
1230 recipient: &PublicKey,
1231 base_tags: &[Tag],
1232 start_tags: Vec<Tag>,
1233 is_encrypted: bool,
1234 gift_wrap_kind: Option<u16>,
1235 ) -> Result<()> {
1236 let chunk_size = resolve_safe_chunk_size(
1243 self.config.oversized_transfer.chunk_size,
1244 &self.base,
1245 recipient,
1246 base_tags,
1247 is_encrypted,
1248 Kind::Custom(gift_wrap_kind.unwrap_or(GIFT_WRAP_KIND)),
1249 self.config.oversized_transfer.threshold,
1250 )
1251 .await?;
1252 let options = OversizedSenderOptions::new(progress_token).with_chunk_size(chunk_size);
1253 let frames = build_oversized_frames(serialized, &options)?.into_ordered();
1254
1255 let mut start_tags = Some(start_tags);
1258 for frame in frames {
1259 let tags = start_tags.take().unwrap_or_else(|| base_tags.to_vec());
1260 let message = JsonRpcMessage::Notification(frame);
1261 self.base
1262 .send_mcp_message(
1263 &message,
1264 recipient,
1265 CTXVM_MESSAGES_KIND,
1266 tags,
1267 Some(is_encrypted),
1268 gift_wrap_kind,
1269 )
1270 .await?;
1271 }
1272 Ok(())
1273 }
1274
1275 pub async fn send_notification(
1277 &self,
1278 client_pubkey_hex: &str,
1279 notification: &JsonRpcMessage,
1280 correlated_event_id: Option<&str>,
1281 ) -> Result<()> {
1282 let mut sessions = self.sessions.write().await;
1283 let session = sessions
1284 .get_mut(client_pubkey_hex)
1285 .ok_or_else(|| Error::Other(format!("No session for {client_pubkey_hex}")))?;
1286 let is_encrypted = session.is_encrypted;
1287 let supports_ephemeral = session.supports_ephemeral_gift_wrap;
1288
1289 let discovery_tags = self.take_pending_server_discovery_tags(session);
1291 drop(sessions);
1292
1293 let client_pubkey =
1294 PublicKey::from_hex(client_pubkey_hex).map_err(|e| Error::Other(e.to_string()))?;
1295
1296 let mut base_tags = BaseTransport::create_recipient_tags(&client_pubkey);
1297 if let Some(eid) = correlated_event_id {
1298 let event_id = EventId::from_hex(eid).map_err(|e| Error::Other(e.to_string()))?;
1299 base_tags.push(Tag::event(event_id));
1300 }
1301
1302 let tags = BaseTransport::compose_outbound_tags(&base_tags, &discovery_tags, &[]);
1303
1304 let correlated_wrap_kind = if let Some(event_id) = correlated_event_id {
1306 self.request_wrap_kinds
1307 .read()
1308 .await
1309 .get(event_id)
1310 .copied()
1311 .flatten()
1312 } else {
1313 None
1314 };
1315
1316 self.base
1317 .send_mcp_message(
1318 notification,
1319 &client_pubkey,
1320 CTXVM_MESSAGES_KIND,
1321 tags,
1322 Some(is_encrypted),
1323 Self::select_outbound_notification_gift_wrap_kind(
1324 self.config.gift_wrap_mode,
1325 is_encrypted,
1326 correlated_wrap_kind,
1327 supports_ephemeral,
1328 ),
1329 )
1330 .await?;
1331
1332 Ok(())
1333 }
1334
1335 pub async fn broadcast_notification(&self, notification: &JsonRpcMessage) -> Result<()> {
1337 let sessions = self.sessions.read().await;
1338 let initialized: Vec<String> = sessions
1339 .iter()
1340 .filter(|(_, s)| s.is_initialized)
1341 .map(|(k, _)| k.clone())
1342 .collect();
1343 drop(sessions);
1344
1345 for pubkey in initialized {
1346 if let Err(error) = self.send_notification(&pubkey, notification, None).await {
1347 tracing::error!(
1348 target: LOG_TARGET,
1349 error = %error,
1350 client_pubkey = %pubkey,
1351 "Failed to send notification"
1352 );
1353 }
1354 }
1355 Ok(())
1356 }
1357
1358 pub fn take_message_receiver(
1360 &mut self,
1361 ) -> Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>> {
1362 self.message_rx.take()
1363 }
1364
1365 pub async fn session_snapshot(&self, client_pubkey: &str) -> Option<SessionSnapshot> {
1369 self.sessions.get_session(client_pubkey).await
1370 }
1371
1372 pub fn set_announcement_extra_tags(&mut self, tags: Vec<Tag>) {
1374 self.announcement_manager.set_extra_common_tags(tags);
1375 }
1376
1377 pub fn set_announcement_pricing_tags(&mut self, tags: Vec<Tag>) {
1379 self.announcement_manager.set_pricing_tags(tags);
1380 }
1381
1382 pub async fn announce(&self) -> Result<EventId> {
1384 self.announcement_manager.announce().await
1385 }
1386
1387 pub async fn publish_tools(&self, tools: Vec<serde_json::Value>) -> Result<EventId> {
1389 self.announcement_manager.publish_tools(tools).await
1390 }
1391
1392 pub async fn publish_resources(&self, resources: Vec<serde_json::Value>) -> Result<EventId> {
1394 self.announcement_manager.publish_resources(resources).await
1395 }
1396
1397 pub async fn publish_prompts(&self, prompts: Vec<serde_json::Value>) -> Result<EventId> {
1399 self.announcement_manager.publish_prompts(prompts).await
1400 }
1401
1402 pub async fn publish_resource_templates(
1404 &self,
1405 templates: Vec<serde_json::Value>,
1406 ) -> Result<EventId> {
1407 self.announcement_manager
1408 .publish_resource_templates(templates)
1409 .await
1410 }
1411
1412 pub async fn delete_announcements(&self, reason: &str) -> Result<()> {
1414 self.announcement_manager.delete_announcements(reason).await
1415 }
1416
1417 #[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
1423 pub(crate) fn spawn_announcements(&mut self) {
1424 if self.config.is_announced_server {
1425 let handle = self
1426 .announcement_manager
1427 .spawn_publish_public_announcements(self.cancellation_token.child_token());
1428 self.task_handles.push(handle);
1429 }
1430 self.spawn_discoverability_publication();
1431 }
1432
1433 pub fn spawn_discoverability_publication(&mut self) {
1439 let handle = self.announcement_manager.spawn_publish_discoverability();
1440 self.task_handles.push(handle);
1441 }
1442
1443 #[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
1447 pub(crate) async fn handle_announcement_response(
1448 &self,
1449 response: JsonRpcMessage,
1450 ) -> Result<()> {
1451 self.announcement_manager
1452 .handle_announcement_response(response)
1453 .await
1454 }
1455
1456 #[cfg(feature = "rmcp")]
1458 pub async fn publish_tools_typed(&self, tools: Vec<rmcp::model::Tool>) -> Result<EventId> {
1459 self.announcement_manager.publish_tools_typed(tools).await
1460 }
1461
1462 #[cfg(feature = "rmcp")]
1464 pub async fn publish_resources_typed(
1465 &self,
1466 resources: Vec<rmcp::model::Resource>,
1467 ) -> Result<EventId> {
1468 self.announcement_manager
1469 .publish_resources_typed(resources)
1470 .await
1471 }
1472
1473 #[cfg(feature = "rmcp")]
1475 pub async fn publish_prompts_typed(
1476 &self,
1477 prompts: Vec<rmcp::model::Prompt>,
1478 ) -> Result<EventId> {
1479 self.announcement_manager
1480 .publish_prompts_typed(prompts)
1481 .await
1482 }
1483
1484 #[cfg(feature = "rmcp")]
1486 pub async fn publish_resource_templates_typed(
1487 &self,
1488 templates: Vec<rmcp::model::ResourceTemplate>,
1489 ) -> Result<EventId> {
1490 self.announcement_manager
1491 .publish_resource_templates_typed(templates)
1492 .await
1493 }
1494
1495 fn take_pending_server_discovery_tags(&self, session: &mut ClientSession) -> Vec<Tag> {
1499 if session.has_sent_common_tags {
1500 return vec![];
1501 }
1502 session.has_sent_common_tags = true;
1503 self.announcement_manager.get_common_tags()
1504 }
1505
1506 fn is_capability_excluded(
1509 excluded: &[CapabilityExclusion],
1510 method: &str,
1511 name: Option<&str>,
1512 ) -> bool {
1513 if method == "initialize" || method == "notifications/initialized" {
1515 return true;
1516 }
1517
1518 excluded.iter().any(|excl| {
1519 if excl.method != method {
1520 return false;
1521 }
1522 match (&excl.name, name) {
1523 (Some(excl_name), Some(req_name)) => excl_name == req_name,
1524 (None, _) => true, _ => false,
1526 }
1527 })
1528 }
1529
1530 async fn sweep_oversized_receivers(
1536 oversized_receiver: &Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
1537 ) {
1538 let mut receivers = oversized_receiver.write().await;
1539 let mut empty_peers: Vec<String> = Vec::new();
1540 for (peer, receiver) in receivers.iter_mut() {
1541 for token in receiver.remove_expired() {
1542 tracing::warn!(
1543 target: LOG_TARGET,
1544 client_pubkey = %peer,
1545 token = %token,
1546 "Oversized transfer reaped by watchdog"
1547 );
1548 }
1549 if receiver.active_transfer_count() == 0 {
1550 empty_peers.push(peer.clone());
1551 }
1552 }
1553 for peer in empty_peers {
1555 receivers.pop(&peer);
1556 }
1557 }
1558
1559 #[allow(clippy::too_many_arguments)]
1560 async fn event_loop(
1561 relay_pool: Arc<dyn RelayPoolTrait>,
1562 sessions: SessionStore,
1563 event_routes: ServerEventRouteStore,
1564 request_wrap_kinds: Arc<RwLock<HashMap<String, Option<u16>>>>,
1565 tx: tokio::sync::mpsc::UnboundedSender<IncomingRequest>,
1566 allowed_pubkeys: Vec<String>,
1567 excluded_capabilities: Vec<CapabilityExclusion>,
1568 encryption_mode: EncryptionMode,
1569 gift_wrap_mode: GiftWrapMode,
1570 is_announced_server: bool,
1571 oversized_enabled: bool,
1572 oversized_receiver: Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
1573 transfer_policy: TransferPolicy,
1574 common_tags_snapshot: announcement_manager::CommonTagsSnapshot,
1575 seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
1576 open_stream: ServerOpenStreamState,
1577 cancel: CancellationToken,
1578 ) {
1579 let mut notifications = relay_pool.notifications();
1580
1581 let watchdog_enabled = oversized_enabled && transfer_policy.transfer_timeout_ms != 0;
1585 let sweep_interval = (Duration::from_millis(transfer_policy.transfer_timeout_ms) / 2)
1586 .clamp(Duration::from_secs(1), Duration::from_secs(30));
1587 let mut sweep_timer =
1588 tokio::time::interval_at(tokio::time::Instant::now() + sweep_interval, sweep_interval);
1589
1590 let open_stream_sweep_enabled =
1594 open_stream.enabled && open_stream.policy.idle_timeout_ms != 0;
1595 let open_stream_sweep_interval =
1596 (Duration::from_millis(open_stream.policy.idle_timeout_ms) / 2)
1597 .clamp(Duration::from_secs(1), Duration::from_secs(30));
1598 let mut open_stream_sweep_timer = tokio::time::interval_at(
1599 tokio::time::Instant::now() + open_stream_sweep_interval,
1600 open_stream_sweep_interval,
1601 );
1602
1603 loop {
1604 let notification = tokio::select! {
1605 _ = cancel.cancelled() => {
1606 tracing::info!(
1607 target: LOG_TARGET,
1608 "Server event loop cancelled"
1609 );
1610 break;
1611 }
1612 _ = sweep_timer.tick(), if watchdog_enabled => {
1613 Self::sweep_oversized_receivers(&oversized_receiver).await;
1614 continue;
1615 }
1616 _ = open_stream_sweep_timer.tick(), if open_stream_sweep_enabled => {
1617 Self::sweep_open_stream_sessions(
1618 &open_stream,
1619 &relay_pool,
1620 encryption_mode,
1621 gift_wrap_mode,
1622 &sessions,
1623 )
1624 .await;
1625 continue;
1626 }
1627 result = notifications.recv() => {
1628 match result {
1629 Ok(n) => n,
1630 Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
1631 tracing::warn!(
1632 target: LOG_TARGET,
1633 skipped = n,
1634 "Relay broadcast lagged, skipping missed events"
1635 );
1636 continue;
1637 }
1638 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
1639 }
1640 }
1641 };
1642 if let RelayPoolNotification::Event { event, .. } = notification {
1643 let is_gift_wrap = event.kind == Kind::Custom(GIFT_WRAP_KIND)
1644 || event.kind == Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND);
1645 let outer_kind: u16 = event.kind.as_u16();
1646
1647 if is_gift_wrap && !gift_wrap_mode.allows_kind(outer_kind) {
1649 tracing::warn!(
1650 target: LOG_TARGET,
1651 event_id = %event.id.to_hex(),
1652 event_kind = outer_kind,
1653 configured_mode = ?gift_wrap_mode,
1654 "Dropping gift-wrap because it violates gift_wrap_mode policy"
1655 );
1656 continue;
1657 }
1658
1659 let (content, sender_pubkey, event_id, is_encrypted, inner_tags, inbound_event) =
1660 if is_gift_wrap {
1661 if encryption_mode == EncryptionMode::Disabled {
1662 tracing::warn!(
1663 target: LOG_TARGET,
1664 event_id = %event.id.to_hex(),
1665 sender_pubkey = %event.pubkey.to_hex(),
1666 "Received encrypted message but encryption is disabled"
1667 );
1668 continue;
1669 }
1670 {
1671 let guard = match seen_gift_wrap_ids.lock() {
1672 Ok(g) => g,
1673 Err(poisoned) => poisoned.into_inner(),
1674 };
1675 if guard.contains(&event.id) {
1676 tracing::debug!(
1677 target: LOG_TARGET,
1678 event_id = %event.id.to_hex(),
1679 "Skipping duplicate gift-wrap (outer id)"
1680 );
1681 continue;
1682 }
1683 }
1684 let signer = match relay_pool.signer().await {
1686 Ok(s) => s,
1687 Err(error) => {
1688 tracing::error!(
1689 target: LOG_TARGET,
1690 error = %error,
1691 "Failed to get signer"
1692 );
1693 continue;
1694 }
1695 };
1696 match encryption::decrypt_gift_wrap_single_layer(&signer, &event).await {
1697 Ok(decrypted_json) => {
1698 match serde_json::from_str::<Event>(&decrypted_json) {
1702 Ok(inner) => {
1703 if let Err(e) = inner.verify() {
1704 tracing::warn!(
1705 "Inner event signature verification failed: {e}"
1706 );
1707 continue;
1708 }
1709 {
1710 let mut guard = match seen_gift_wrap_ids.lock() {
1711 Ok(g) => g,
1712 Err(poisoned) => poisoned.into_inner(),
1713 };
1714 guard.put(event.id, ());
1715 }
1716 let inbound = inner.clone();
1717 let inner_tags: Vec<Tag> = inner.tags.to_vec();
1718 (
1719 inner.content,
1720 inner.pubkey.to_hex(),
1721 inner.id.to_hex(),
1722 true,
1723 inner_tags,
1724 Some(inbound),
1725 )
1726 }
1727 Err(error) => {
1728 tracing::error!(
1729 target: LOG_TARGET,
1730 error = %error,
1731 "Failed to parse inner event"
1732 );
1733 continue;
1734 }
1735 }
1736 }
1737 Err(error) => {
1738 tracing::error!(
1739 target: LOG_TARGET,
1740 error = %error,
1741 "Failed to decrypt"
1742 );
1743 continue;
1744 }
1745 }
1746 } else {
1747 if encryption_mode == EncryptionMode::Required {
1748 tracing::warn!(
1749 target: LOG_TARGET,
1750 sender_pubkey = %event.pubkey.to_hex(),
1751 "Received unencrypted message but encryption is required"
1752 );
1753 continue;
1754 }
1755 if let Err(e) = event.verify() {
1761 tracing::warn!(
1762 target: LOG_TARGET,
1763 "Plaintext event signature verification failed: {e}"
1764 );
1765 continue;
1766 }
1767 let inbound = (*event).clone();
1768 (
1769 event.content.clone(),
1770 event.pubkey.to_hex(),
1771 event.id.to_hex(),
1772 false,
1773 event.tags.to_vec(),
1774 Some(inbound),
1775 )
1776 };
1777
1778 let mcp_msg = match validation::validate_and_parse(&content) {
1780 Some(msg) => msg,
1781 None => {
1782 tracing::warn!(
1783 target: LOG_TARGET,
1784 sender_pubkey = %sender_pubkey,
1785 "Invalid MCP message"
1786 );
1787 continue;
1788 }
1789 };
1790
1791 if !allowed_pubkeys.is_empty() {
1793 let method = mcp_msg.method().unwrap_or("");
1794 let name = match &mcp_msg {
1795 JsonRpcMessage::Request(r) => r
1796 .params
1797 .as_ref()
1798 .and_then(|p| p.get("name"))
1799 .and_then(|n| n.as_str()),
1800 _ => None,
1801 };
1802
1803 let is_excluded =
1804 Self::is_capability_excluded(&excluded_capabilities, method, name);
1805
1806 if !allowed_pubkeys.contains(&sender_pubkey) && !is_excluded {
1807 tracing::warn!(
1808 target: LOG_TARGET,
1809 sender_pubkey = %sender_pubkey,
1810 method = method,
1811 "Unauthorized request"
1812 );
1813
1814 if is_announced_server {
1817 if let JsonRpcMessage::Request(ref req) = mcp_msg {
1818 if let Ok(client_pk) = PublicKey::from_hex(&sender_pubkey) {
1819 let event_id_parsed = EventId::from_hex(&event_id)
1820 .unwrap_or(EventId::all_zeros());
1821 let mut tags = BaseTransport::create_response_tags(
1822 &client_pk,
1823 &event_id_parsed,
1824 );
1825
1826 let has_sent = sessions
1828 .get_session(&sender_pubkey)
1829 .await
1830 .is_some_and(|s| s.has_sent_common_tags);
1831 if !has_sent {
1832 common_tags_snapshot.append_common_response_tags(&mut tags);
1833 sessions.mark_common_tags_sent(&sender_pubkey).await;
1834 }
1835
1836 let error_response =
1837 JsonRpcMessage::ErrorResponse(JsonRpcErrorResponse {
1838 jsonrpc: "2.0".to_string(),
1839 id: req.id.clone(),
1840 error: JsonRpcError {
1841 code: -32000,
1842 message: "Unauthorized".to_string(),
1843 data: None,
1844 },
1845 });
1846
1847 let base = BaseTransport {
1848 relay_pool: Arc::clone(&relay_pool),
1849 encryption_mode,
1850 is_connected: true,
1851 };
1852 if let Err(e) = base
1853 .send_mcp_message(
1854 &error_response,
1855 &client_pk,
1856 CTXVM_MESSAGES_KIND,
1857 tags,
1858 Some(is_encrypted),
1859 Self::select_outbound_gift_wrap_kind(
1860 gift_wrap_mode,
1861 is_encrypted,
1862 if is_gift_wrap { Some(outer_kind) } else { None },
1863 ),
1864 )
1865 .await
1866 {
1867 tracing::error!(
1868 target: LOG_TARGET,
1869 error = %e,
1870 sender_pubkey = %sender_pubkey,
1871 "Failed to send unauthorized error response"
1872 );
1873 }
1874 }
1875 }
1876 } continue;
1879 }
1880 }
1881
1882 let on_evicted_cb = sessions.eviction_callback();
1884 let mut sessions_w = sessions.write().await;
1885 if !sessions_w.contains(&sender_pubkey) {
1886 let evicted =
1887 sessions_w.push(sender_pubkey.clone(), ClientSession::new(is_encrypted));
1888 SessionStore::handle_eviction(
1889 &sender_pubkey,
1890 evicted,
1891 &mut sessions_w,
1892 on_evicted_cb.as_ref(),
1893 &event_routes,
1894 )
1895 .await;
1896 }
1897 let session = sessions_w.get_mut(&sender_pubkey).unwrap();
1898 session.update_activity();
1899 session.is_encrypted = is_encrypted;
1900
1901 if is_gift_wrap && outer_kind == EPHEMERAL_GIFT_WRAP_KIND {
1903 session.supports_ephemeral_gift_wrap = true;
1904 }
1905
1906 let discovered = learn_peer_capabilities(&inner_tags);
1908 session.supports_encryption |= discovered.supports_encryption;
1909 session.supports_ephemeral_encryption |= discovered.supports_ephemeral_encryption;
1910 let client_already_supported = session.supports_oversized_transfer;
1914 session.supports_oversized_transfer |=
1916 oversized_enabled && discovered.supports_oversized_transfer;
1917 session.supports_open_stream |=
1921 open_stream.enabled && discovered.supports_open_stream;
1922 let client_supports_open_stream = session.supports_open_stream;
1923
1924 if oversized_enabled {
1928 if let JsonRpcMessage::Notification(ref n) = mcp_msg {
1929 if OversizedTransferReceiver::is_oversized_frame(n) {
1930 drop(sessions_w);
1931 Self::handle_oversized_frame(
1932 n,
1933 &sender_pubkey,
1934 &event_id,
1935 is_encrypted,
1936 is_gift_wrap,
1937 outer_kind,
1938 client_already_supported,
1939 &oversized_receiver,
1940 transfer_policy,
1941 &relay_pool,
1942 encryption_mode,
1943 gift_wrap_mode,
1944 &event_routes,
1945 &request_wrap_kinds,
1946 &tx,
1947 &open_stream,
1948 inbound_event,
1949 )
1950 .await;
1951 continue;
1952 }
1953 }
1954 }
1955
1956 if open_stream.enabled {
1961 if let JsonRpcMessage::Notification(ref n) = mcp_msg {
1962 if OpenStreamReceiver::is_open_stream_frame(n) {
1963 drop(sessions_w);
1964 Self::handle_open_stream_frame(
1965 &open_stream,
1966 &relay_pool,
1967 encryption_mode,
1968 gift_wrap_mode,
1969 n,
1970 &sender_pubkey,
1971 &event_id,
1972 is_encrypted,
1973 is_gift_wrap,
1974 outer_kind,
1975 client_supports_open_stream,
1976 )
1977 .await;
1978 continue;
1979 }
1980 }
1981 }
1982
1983 if let JsonRpcMessage::Request(ref req) = mcp_msg {
1985 let original_id = req.id.clone();
1986
1987 let progress_token = req
1994 .params
1995 .as_ref()
1996 .and_then(|p| p.get("_meta"))
1997 .and_then(|m| m.get("progressToken"))
1998 .and_then(progress_token_string);
1999
2000 session
2002 .pending_requests
2003 .insert(event_id.clone(), original_id.clone());
2004 if let Some(ref token) = progress_token {
2005 session
2006 .pending_requests
2007 .insert(token.clone(), serde_json::json!(event_id));
2008 session
2009 .event_to_progress_token
2010 .insert(event_id.clone(), token.clone());
2011 }
2012
2013 drop(sessions_w);
2014
2015 {
2017 let mut kinds_w = request_wrap_kinds.write().await;
2018 kinds_w.insert(
2019 event_id.clone(),
2020 if is_gift_wrap { Some(outer_kind) } else { None },
2021 );
2022 }
2023
2024 let writer_request_id = original_id.clone();
2027 let writer_token = progress_token.clone();
2028
2029 event_routes
2030 .register(
2031 event_id.clone(),
2032 sender_pubkey.clone(),
2033 original_id,
2034 progress_token,
2035 )
2036 .await;
2037
2038 if open_stream.enabled && req.method == "tools/call" {
2043 if let Some(token) = writer_token {
2044 Self::create_open_stream_writer(
2045 &open_stream,
2046 &relay_pool,
2047 encryption_mode,
2048 gift_wrap_mode,
2049 &event_id,
2050 &sender_pubkey,
2051 &token,
2052 writer_request_id,
2053 is_encrypted,
2054 if is_gift_wrap { Some(outer_kind) } else { None },
2055 );
2056 }
2057 }
2058 } else {
2059 drop(sessions_w);
2060 }
2061
2062 if let JsonRpcMessage::Notification(ref n) = mcp_msg {
2064 if n.method == "notifications/initialized" {
2065 let mut sessions_w2 = sessions.write().await;
2066 if let Some(session) = sessions_w2.get_mut(&sender_pubkey) {
2067 session.is_initialized = true;
2068 }
2069 }
2070 }
2071
2072 let _ = tx.send(IncomingRequest {
2074 message: mcp_msg,
2075 client_pubkey: sender_pubkey,
2076 event_id,
2077 is_encrypted,
2078 event: inbound_event,
2079 });
2080 }
2081 }
2082 }
2083
2084 #[allow(clippy::too_many_arguments)]
2092 async fn handle_oversized_frame(
2093 frame: &JsonRpcNotification,
2094 sender_pubkey: &str,
2095 event_id: &str,
2096 is_encrypted: bool,
2097 is_gift_wrap: bool,
2098 outer_kind: u16,
2099 client_already_supported: bool,
2100 oversized_receiver: &Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
2101 transfer_policy: TransferPolicy,
2102 relay_pool: &Arc<dyn RelayPoolTrait>,
2103 encryption_mode: EncryptionMode,
2104 gift_wrap_mode: GiftWrapMode,
2105 event_routes: &ServerEventRouteStore,
2106 request_wrap_kinds: &Arc<RwLock<HashMap<String, Option<u16>>>>,
2107 tx: &tokio::sync::mpsc::UnboundedSender<IncomingRequest>,
2108 open_stream: &ServerOpenStreamState,
2109 inbound_event: Option<Event>,
2110 ) {
2111 let token = frame
2115 .params
2116 .as_ref()
2117 .and_then(|p| p.get("progressToken"))
2118 .and_then(progress_token_string);
2119
2120 let is_start = frame
2122 .params
2123 .as_ref()
2124 .and_then(|p| p.get("cvm"))
2125 .and_then(OversizedFrame::from_cvm_value)
2126 .is_some_and(|f| matches!(f, OversizedFrame::Start { .. }));
2127 let issued_accept = is_start && !client_already_supported && token.is_some();
2128 if issued_accept {
2129 if let Some(ref token) = token {
2130 Self::emit_accept_frame(
2131 token,
2132 sender_pubkey,
2133 event_id,
2134 is_encrypted,
2135 is_gift_wrap,
2136 outer_kind,
2137 relay_pool,
2138 encryption_mode,
2139 gift_wrap_mode,
2140 )
2141 .await;
2142 }
2143 }
2144
2145 let outcome = {
2153 let mut store = oversized_receiver.write().await;
2154 if !store.contains(sender_pubkey) {
2155 store.put(
2156 sender_pubkey.to_string(),
2157 OversizedTransferReceiver::with_policy(transfer_policy),
2158 );
2159 }
2160 let receiver = store.get_mut(sender_pubkey).unwrap();
2161 let outcome = receiver.process_frame(frame);
2162 if issued_accept && matches!(outcome, Ok(None)) {
2163 if let Some(ref token) = token {
2164 if let Ok(accept) = OversizedFrame::Accept.into_progress_notification(
2165 token,
2166 ACCEPT_PROGRESS,
2167 None,
2168 ) {
2169 let _ = receiver.process_frame(&accept);
2170 }
2171 }
2172 }
2173 outcome
2174 };
2175
2176 match outcome {
2177 Ok(None) => {}
2179 Ok(Some(message)) => {
2181 let original_id = message.id().cloned().unwrap_or(serde_json::Value::Null);
2182 let writer_token = match &message {
2187 JsonRpcMessage::Request(req) if req.method == "tools/call" => req
2188 .params
2189 .as_ref()
2190 .and_then(|p| p.get("_meta"))
2191 .and_then(|m| m.get("progressToken"))
2192 .and_then(progress_token_string),
2193 _ => None,
2194 };
2195 let writer_request_id = original_id.clone();
2196 {
2198 let mut kinds_w = request_wrap_kinds.write().await;
2199 kinds_w.insert(
2200 event_id.to_string(),
2201 if is_gift_wrap { Some(outer_kind) } else { None },
2202 );
2203 }
2204 event_routes
2205 .register(
2206 event_id.to_string(),
2207 sender_pubkey.to_string(),
2208 original_id,
2209 token,
2210 )
2211 .await;
2212 if open_stream.enabled {
2213 if let Some(progress_token) = writer_token {
2214 Self::create_open_stream_writer(
2215 open_stream,
2216 relay_pool,
2217 encryption_mode,
2218 gift_wrap_mode,
2219 event_id,
2220 sender_pubkey,
2221 &progress_token,
2222 writer_request_id,
2223 is_encrypted,
2224 if is_gift_wrap { Some(outer_kind) } else { None },
2225 );
2226 }
2227 }
2228 let _ = tx.send(IncomingRequest {
2229 message,
2230 client_pubkey: sender_pubkey.to_string(),
2231 event_id: event_id.to_string(),
2232 is_encrypted,
2233 event: inbound_event,
2234 });
2235 }
2236 Err(error) => {
2238 tracing::warn!(
2239 target: LOG_TARGET,
2240 error = %error,
2241 sender_pubkey = %sender_pubkey,
2242 "Oversized transfer frame rejected; cleaning up locally"
2243 );
2244 }
2245 }
2246 }
2247
2248 #[allow(clippy::too_many_arguments)]
2252 async fn emit_accept_frame(
2253 token: &str,
2254 sender_pubkey: &str,
2255 start_event_id: &str,
2256 is_encrypted: bool,
2257 is_gift_wrap: bool,
2258 outer_kind: u16,
2259 relay_pool: &Arc<dyn RelayPoolTrait>,
2260 encryption_mode: EncryptionMode,
2261 gift_wrap_mode: GiftWrapMode,
2262 ) {
2263 let client_pk = match PublicKey::from_hex(sender_pubkey) {
2264 Ok(pk) => pk,
2265 Err(_) => return,
2266 };
2267 let event_id_parsed = EventId::from_hex(start_event_id).unwrap_or(EventId::all_zeros());
2268 let accept = match OversizedFrame::Accept.into_progress_notification(
2269 token,
2270 ACCEPT_PROGRESS,
2271 Some("oversized request accepted"),
2272 ) {
2273 Ok(n) => JsonRpcMessage::Notification(n),
2274 Err(error) => {
2275 tracing::error!(
2276 target: LOG_TARGET,
2277 error = %error,
2278 "Failed to build oversized-transfer accept frame"
2279 );
2280 return;
2281 }
2282 };
2283 let tags = BaseTransport::create_response_tags(&client_pk, &event_id_parsed);
2284 let base = BaseTransport {
2285 relay_pool: Arc::clone(relay_pool),
2286 encryption_mode,
2287 is_connected: true,
2288 };
2289 if let Err(error) = base
2290 .send_mcp_message(
2291 &accept,
2292 &client_pk,
2293 CTXVM_MESSAGES_KIND,
2294 tags,
2295 Some(is_encrypted),
2296 Self::select_outbound_gift_wrap_kind(
2297 gift_wrap_mode,
2298 is_encrypted,
2299 if is_gift_wrap { Some(outer_kind) } else { None },
2300 ),
2301 )
2302 .await
2303 {
2304 tracing::error!(
2305 target: LOG_TARGET,
2306 error = %error,
2307 sender_pubkey = %sender_pubkey,
2308 "Failed to send oversized-transfer accept frame"
2309 );
2310 }
2311 }
2312
2313 #[allow(clippy::too_many_arguments)]
2319 fn create_open_stream_writer(
2320 state: &ServerOpenStreamState,
2321 relay_pool: &Arc<dyn RelayPoolTrait>,
2322 encryption_mode: EncryptionMode,
2323 gift_wrap_mode: GiftWrapMode,
2324 event_id: &str,
2325 client_pubkey_hex: &str,
2326 progress_token: &str,
2327 original_request_id: serde_json::Value,
2328 is_encrypted: bool,
2329 mirrored_wrap_kind: Option<u16>,
2330 ) {
2331 let client_pubkey = match PublicKey::from_hex(client_pubkey_hex) {
2332 Ok(pk) => pk,
2333 Err(_) => return,
2334 };
2335 let event_id_parsed = match EventId::from_hex(event_id) {
2336 Ok(id) => id,
2337 Err(_) => return,
2338 };
2339 let gift_wrap_kind =
2340 Self::select_outbound_gift_wrap_kind(gift_wrap_mode, is_encrypted, mirrored_wrap_kind);
2341
2342 let publish_relay_pool = Arc::clone(relay_pool);
2348 let publish_frame: PublishFrame = Arc::new(move |notification: JsonRpcNotification| {
2349 let relay_pool = Arc::clone(&publish_relay_pool);
2350 Box::pin(async move {
2351 let base = BaseTransport {
2352 relay_pool,
2353 encryption_mode,
2354 is_connected: true,
2355 };
2356 let tags = BaseTransport::create_response_tags(&client_pubkey, &event_id_parsed);
2357 let message = JsonRpcMessage::Notification(notification);
2358 base.send_mcp_message(
2359 &message,
2360 &client_pubkey,
2361 CTXVM_MESSAGES_KIND,
2362 tags,
2363 Some(is_encrypted),
2364 gift_wrap_kind,
2365 )
2366 .await
2367 })
2368 });
2369
2370 let on_close: OnCloseHook = {
2372 let state = state.clone();
2373 let relay_pool = Arc::clone(relay_pool);
2374 let event_id = event_id.to_string();
2375 Arc::new(move || {
2376 let state = state.clone();
2377 let relay_pool = Arc::clone(&relay_pool);
2378 let event_id = event_id.clone();
2379 Box::pin(async move {
2380 let base = BaseTransport {
2381 relay_pool,
2382 encryption_mode,
2383 is_connected: true,
2384 };
2385 Self::flush_open_stream_response(&state, &base, gift_wrap_mode, &event_id)
2386 .await;
2387 })
2388 })
2389 };
2390 let on_abort: OnAbortHook = {
2391 let state = state.clone();
2392 let relay_pool = Arc::clone(relay_pool);
2393 let event_id = event_id.to_string();
2394 Arc::new(move |_reason| {
2395 let state = state.clone();
2396 let relay_pool = Arc::clone(&relay_pool);
2397 let event_id = event_id.clone();
2398 Box::pin(async move {
2399 let base = BaseTransport {
2400 relay_pool,
2401 encryption_mode,
2402 is_connected: true,
2403 };
2404 Self::flush_open_stream_response(&state, &base, gift_wrap_mode, &event_id)
2405 .await;
2406 })
2407 })
2408 };
2409
2410 let writer = OpenStreamWriter::new(OpenStreamWriterOptions {
2411 progress_token: progress_token.to_string(),
2412 publish_frame,
2413 content_type: None,
2414 on_close: Some(on_close),
2415 on_abort: Some(on_abort),
2416 idle_timeout: (state.policy.idle_timeout_ms != 0)
2421 .then(|| Duration::from_millis(state.policy.idle_timeout_ms)),
2422 probe_timeout: Duration::from_millis(state.policy.probe_timeout_ms),
2423 });
2424 let snapshot = RouteSnapshot {
2425 client_pubkey,
2426 original_request_id,
2427 is_encrypted,
2428 mirrored_wrap_kind,
2429 };
2430 state.lock_slots().insert(
2431 event_id.to_string(),
2432 OpenStreamSlot {
2433 writer,
2434 snapshot,
2435 pending_response: None,
2436 terminated: false,
2437 },
2438 );
2439 state
2440 .lock_token_index()
2441 .insert(progress_token.to_string(), event_id.to_string());
2442 }
2443
2444 #[allow(clippy::too_many_arguments)]
2448 async fn handle_open_stream_frame(
2449 state: &ServerOpenStreamState,
2450 relay_pool: &Arc<dyn RelayPoolTrait>,
2451 encryption_mode: EncryptionMode,
2452 gift_wrap_mode: GiftWrapMode,
2453 notification: &JsonRpcNotification,
2454 sender_pubkey: &str,
2455 event_id: &str,
2456 is_encrypted: bool,
2457 is_gift_wrap: bool,
2458 outer_kind: u16,
2459 client_supports_open_stream: bool,
2460 ) {
2461 let token = notification
2462 .params
2463 .as_ref()
2464 .and_then(|p| p.get("progressToken"))
2465 .and_then(progress_token_string);
2466 let writer = token
2468 .as_deref()
2469 .and_then(|t| state.event_id_for_token(t))
2470 .and_then(|eid| state.writer_for(&eid));
2471
2472 match open_stream_frame_from_notification(notification) {
2473 Some(OpenStreamFrame::Ping { nonce }) => {
2474 if let Some(writer) = writer {
2475 let _ = writer.pong(nonce).await;
2476 } else {
2477 Self::feed_open_stream_reader(
2478 state,
2479 relay_pool,
2480 encryption_mode,
2481 gift_wrap_mode,
2482 notification,
2483 sender_pubkey,
2484 event_id,
2485 is_encrypted,
2486 is_gift_wrap,
2487 outer_kind,
2488 )
2489 .await;
2490 }
2491 }
2492 Some(OpenStreamFrame::Abort { reason }) => {
2493 if let Some(writer) = writer {
2494 let _ = writer.abort(reason).await;
2495 } else {
2496 Self::feed_open_stream_reader(
2497 state,
2498 relay_pool,
2499 encryption_mode,
2500 gift_wrap_mode,
2501 notification,
2502 sender_pubkey,
2503 event_id,
2504 is_encrypted,
2505 is_gift_wrap,
2506 outer_kind,
2507 )
2508 .await;
2509 }
2510 }
2511 Some(OpenStreamFrame::Pong { nonce }) => {
2512 if let Some(writer) = writer {
2517 writer.ack_probe(&nonce);
2518 } else {
2519 Self::feed_open_stream_reader(
2520 state,
2521 relay_pool,
2522 encryption_mode,
2523 gift_wrap_mode,
2524 notification,
2525 sender_pubkey,
2526 event_id,
2527 is_encrypted,
2528 is_gift_wrap,
2529 outer_kind,
2530 )
2531 .await;
2532 }
2533 }
2534 Some(OpenStreamFrame::Start { .. }) => {
2535 Self::feed_open_stream_reader(
2536 state,
2537 relay_pool,
2538 encryption_mode,
2539 gift_wrap_mode,
2540 notification,
2541 sender_pubkey,
2542 event_id,
2543 is_encrypted,
2544 is_gift_wrap,
2545 outer_kind,
2546 )
2547 .await;
2548 if client_supports_open_stream {
2550 if let Some(token) = token.as_deref() {
2551 Self::publish_open_stream_control_frame(
2552 state,
2553 relay_pool,
2554 encryption_mode,
2555 gift_wrap_mode,
2556 OpenStreamFrame::Accept,
2557 token,
2558 sender_pubkey,
2559 Some(event_id),
2560 is_encrypted,
2561 is_gift_wrap,
2562 outer_kind,
2563 )
2564 .await;
2565 }
2566 }
2567 }
2568 _ => {
2570 Self::feed_open_stream_reader(
2571 state,
2572 relay_pool,
2573 encryption_mode,
2574 gift_wrap_mode,
2575 notification,
2576 sender_pubkey,
2577 event_id,
2578 is_encrypted,
2579 is_gift_wrap,
2580 outer_kind,
2581 )
2582 .await;
2583 }
2584 }
2585 }
2586
2587 #[allow(clippy::too_many_arguments)]
2590 async fn feed_open_stream_reader(
2591 state: &ServerOpenStreamState,
2592 relay_pool: &Arc<dyn RelayPoolTrait>,
2593 encryption_mode: EncryptionMode,
2594 gift_wrap_mode: GiftWrapMode,
2595 notification: &JsonRpcNotification,
2596 sender_pubkey: &str,
2597 event_id: &str,
2598 is_encrypted: bool,
2599 is_gift_wrap: bool,
2600 outer_kind: u16,
2601 ) {
2602 let outcome = {
2603 let mut store = state.receiver.lock().await;
2604 if !store.contains(sender_pubkey) {
2605 store.put(
2606 sender_pubkey.to_string(),
2607 OpenStreamReceiver::with_policy(state.policy),
2608 );
2609 }
2610 let receiver = store
2611 .get_mut(sender_pubkey)
2612 .expect("open-stream receiver present after insert");
2613 receiver.process_frame(notification).await
2614 };
2615 match outcome {
2616 Ok(FrameOutcome::SendPong(nonce)) => {
2617 if let Some(token) = notification
2618 .params
2619 .as_ref()
2620 .and_then(|p| p.get("progressToken"))
2621 .and_then(progress_token_string)
2622 {
2623 Self::publish_open_stream_control_frame(
2624 state,
2625 relay_pool,
2626 encryption_mode,
2627 gift_wrap_mode,
2628 OpenStreamFrame::Pong { nonce },
2629 &token,
2630 sender_pubkey,
2631 Some(event_id),
2632 is_encrypted,
2633 is_gift_wrap,
2634 outer_kind,
2635 )
2636 .await;
2637 }
2638 }
2639 Ok(_) => {}
2640 Err(error) => {
2641 tracing::warn!(
2642 target: LOG_TARGET,
2643 error = %error,
2644 sender_pubkey = %sender_pubkey,
2645 "Inbound open-stream frame rejected by server reader engine"
2646 );
2647 }
2648 }
2649 }
2650
2651 #[allow(clippy::too_many_arguments)]
2655 async fn publish_open_stream_control_frame(
2656 state: &ServerOpenStreamState,
2657 relay_pool: &Arc<dyn RelayPoolTrait>,
2658 encryption_mode: EncryptionMode,
2659 gift_wrap_mode: GiftWrapMode,
2660 frame: OpenStreamFrame,
2661 token: &str,
2662 recipient_pubkey: &str,
2663 correlated_event_id: Option<&str>,
2664 is_encrypted: bool,
2665 is_gift_wrap: bool,
2666 outer_kind: u16,
2667 ) {
2668 let recipient = match PublicKey::from_hex(recipient_pubkey) {
2669 Ok(pk) => pk,
2670 Err(_) => return,
2671 };
2672 let progress = state.next_control_progress();
2673 let notification = match frame.into_progress_notification(token, progress, None) {
2674 Ok(n) => n,
2675 Err(error) => {
2676 tracing::error!(
2677 target: LOG_TARGET,
2678 error = %error,
2679 "Failed to build open-stream control frame"
2680 );
2681 return;
2682 }
2683 };
2684 let mut tags = BaseTransport::create_recipient_tags(&recipient);
2685 if let Some(eid) = correlated_event_id.and_then(|id| EventId::from_hex(id).ok()) {
2689 tags.push(Tag::event(eid));
2690 }
2691 let base = BaseTransport {
2692 relay_pool: Arc::clone(relay_pool),
2693 encryption_mode,
2694 is_connected: true,
2695 };
2696 let gift_wrap_kind = Self::select_outbound_gift_wrap_kind(
2697 gift_wrap_mode,
2698 is_encrypted,
2699 if is_gift_wrap { Some(outer_kind) } else { None },
2700 );
2701 if let Err(error) = base
2702 .send_mcp_message(
2703 &JsonRpcMessage::Notification(notification),
2704 &recipient,
2705 CTXVM_MESSAGES_KIND,
2706 tags,
2707 Some(is_encrypted),
2708 gift_wrap_kind,
2709 )
2710 .await
2711 {
2712 tracing::warn!(
2713 target: LOG_TARGET,
2714 error = %error,
2715 "Failed to publish open-stream control frame"
2716 );
2717 }
2718 }
2719
2720 async fn sweep_open_stream_sessions(
2725 state: &ServerOpenStreamState,
2726 relay_pool: &Arc<dyn RelayPoolTrait>,
2727 encryption_mode: EncryptionMode,
2728 gift_wrap_mode: GiftWrapMode,
2729 sessions: &SessionStore,
2730 ) {
2731 let now = Instant::now();
2732 let mut actions: Vec<(String, String, KeepaliveAction)> = Vec::new();
2733 {
2734 let mut store = state.receiver.lock().await;
2735 let mut empty_peers = Vec::new();
2736 for (peer, receiver) in store.iter_mut() {
2737 for (token, action) in receiver.registry_mut().tick_all(now) {
2738 actions.push((peer.clone(), token, action));
2739 }
2740 if receiver.active_stream_count() == 0 {
2741 empty_peers.push(peer.clone());
2742 }
2743 }
2744 for peer in empty_peers {
2745 store.pop(&peer);
2746 }
2747 }
2748
2749 let probe_is_encrypted = encryption_mode != EncryptionMode::Disabled;
2750 for (peer, token, action) in actions {
2751 match action {
2752 KeepaliveAction::SendPing(nonce) => {
2753 let correlated = state.event_id_for_token(&token);
2758 Self::publish_open_stream_control_frame(
2759 state,
2760 relay_pool,
2761 encryption_mode,
2762 gift_wrap_mode,
2763 OpenStreamFrame::Ping { nonce },
2764 &token,
2765 &peer,
2766 correlated.as_deref(),
2767 probe_is_encrypted,
2768 false,
2769 0,
2770 )
2771 .await;
2772 }
2773 KeepaliveAction::Abort(reason) => {
2774 if let Some(eid) = state.event_id_for_token(&token) {
2775 if let Some(writer) = state.writer_for(&eid) {
2776 let _ = writer.abort(Some(reason)).await;
2777 }
2778 }
2779 }
2780 KeepaliveAction::None => {}
2781 }
2782 }
2783
2784 let writer_actions: Vec<(String, PublicKey, KeepaliveAction)> = state
2793 .lock_slots()
2794 .iter()
2795 .map(|(event_id, slot)| {
2796 (
2797 event_id.clone(),
2798 slot.snapshot.client_pubkey,
2799 slot.writer.tick(now),
2800 )
2801 })
2802 .filter(|(.., action)| !matches!(action, KeepaliveAction::None))
2803 .collect();
2804 for (event_id, client_pubkey, action) in writer_actions {
2805 let Some(writer) = state.writer_for(&event_id) else {
2806 continue;
2807 };
2808 match action {
2809 KeepaliveAction::SendPing(nonce) => {
2810 if let Err(error) = writer.send_probe(nonce).await {
2811 tracing::warn!(
2812 target: LOG_TARGET,
2813 error = %error,
2814 event_id = %event_id,
2815 "Failed to publish open-stream keepalive ping"
2816 );
2817 }
2818 }
2819 KeepaliveAction::Abort(reason) => {
2820 if let Err(error) = writer.abort(Some(reason)).await {
2821 tracing::warn!(
2822 target: LOG_TARGET,
2823 error = %error,
2824 event_id = %event_id,
2825 "Failed to abort open-stream writer on probe timeout"
2826 );
2827 }
2828 let pubkey_hex = client_pubkey.to_hex();
2836 if sessions.remove_session(&pubkey_hex).await {
2837 if let Some(cb) = sessions.eviction_callback() {
2838 cb(pubkey_hex);
2839 }
2840 }
2841 }
2842 KeepaliveAction::None => {}
2843 }
2844 }
2845 }
2846
2847 async fn cleanup_sessions(
2848 sessions: &SessionStore,
2849 event_routes: &ServerEventRouteStore,
2850 request_wrap_kinds: &Arc<RwLock<HashMap<String, Option<u16>>>>,
2851 timeout: Duration,
2852 ) -> usize {
2853 let mut sessions_w = sessions.write().await;
2854 let mut cleaned = 0;
2855 let mut stale_event_ids = Vec::new();
2856
2857 let expired_keys: Vec<String> = sessions_w
2859 .iter()
2860 .filter(|(_, session)| session.last_activity.elapsed() > timeout)
2861 .map(|(k, _)| k.clone())
2862 .collect();
2863
2864 for key in &expired_keys {
2865 if let Some(session) = sessions_w.pop(key) {
2866 stale_event_ids.extend(session.pending_requests.keys().cloned());
2867 stale_event_ids.extend(session.event_to_progress_token.keys().cloned());
2868 tracing::debug!(
2869 target: LOG_TARGET,
2870 client_pubkey = %key,
2871 "Session expired"
2872 );
2873 cleaned += 1;
2874 }
2875 }
2876 drop(sessions_w);
2877
2878 {
2879 let mut kinds_w = request_wrap_kinds.write().await;
2880 for event_id in &stale_event_ids {
2881 kinds_w.remove(event_id);
2882 }
2883 }
2884
2885 for event_id in &stale_event_ids {
2886 event_routes.pop(event_id).await;
2887 }
2888
2889 cleaned
2890 }
2891
2892 fn select_outbound_gift_wrap_kind(
2896 mode: GiftWrapMode,
2897 is_encrypted: bool,
2898 mirrored_kind: Option<u16>,
2899 ) -> Option<u16> {
2900 if !is_encrypted {
2901 return None;
2902 }
2903 if let Some(kind) = mirrored_kind {
2904 if mode.allows_kind(kind) {
2905 return Some(kind);
2906 }
2907 }
2908 match mode {
2909 GiftWrapMode::Persistent => Some(GIFT_WRAP_KIND),
2910 GiftWrapMode::Ephemeral => Some(EPHEMERAL_GIFT_WRAP_KIND),
2911 GiftWrapMode::Optional => Some(GIFT_WRAP_KIND),
2912 }
2913 }
2914
2915 fn select_outbound_notification_gift_wrap_kind(
2917 mode: GiftWrapMode,
2918 is_encrypted: bool,
2919 correlated_wrap_kind: Option<u16>,
2920 client_supports_ephemeral: bool,
2921 ) -> Option<u16> {
2922 if !is_encrypted {
2923 return None;
2924 }
2925 if let Some(kind) = correlated_wrap_kind {
2927 if mode.allows_kind(kind) {
2928 return Some(kind);
2929 }
2930 }
2931 if client_supports_ephemeral && mode.supports_ephemeral() {
2933 return Some(EPHEMERAL_GIFT_WRAP_KIND);
2934 }
2935 match mode {
2936 GiftWrapMode::Persistent => Some(GIFT_WRAP_KIND),
2937 GiftWrapMode::Ephemeral => Some(EPHEMERAL_GIFT_WRAP_KIND),
2938 GiftWrapMode::Optional => Some(GIFT_WRAP_KIND),
2939 }
2940 }
2941}
2942
2943#[cfg(test)]
2944mod tests {
2945 use super::*;
2946 use crate::relay::mock::MockRelayPool;
2947 use std::thread;
2948
2949 #[test]
2952 fn test_client_session_creation() {
2953 let session = ClientSession::new(true);
2954 assert!(!session.is_initialized);
2955 assert!(session.is_encrypted);
2956 assert!(!session.has_sent_common_tags);
2957 assert!(!session.supports_ephemeral_gift_wrap);
2958 assert!(session.pending_requests.is_empty());
2959 assert!(session.event_to_progress_token.is_empty());
2960 }
2961
2962 #[test]
2963 fn test_client_session_update_activity() {
2964 let mut session = ClientSession::new(false);
2965 let first = session.last_activity;
2966 thread::sleep(Duration::from_millis(10));
2967 session.update_activity();
2968 assert!(session.last_activity > first);
2969 }
2970
2971 #[tokio::test]
2972 async fn test_cleanup_sessions_removes_expired() {
2973 let sessions = SessionStore::new();
2974 let event_routes = ServerEventRouteStore::new();
2975
2976 let mut session = ClientSession::new(false);
2978 session
2979 .pending_requests
2980 .insert("evt1".to_string(), serde_json::json!(1));
2981 sessions.write().await.put("pubkey1".to_string(), session);
2982 event_routes
2983 .register(
2984 "evt1".to_string(),
2985 "pubkey1".to_string(),
2986 serde_json::json!(1),
2987 None,
2988 )
2989 .await;
2990
2991 let request_wrap_kinds = Arc::new(RwLock::new(HashMap::new()));
2992
2993 let cleaned = NostrServerTransport::cleanup_sessions(
2995 &sessions,
2996 &event_routes,
2997 &request_wrap_kinds,
2998 Duration::from_secs(300),
2999 )
3000 .await;
3001 assert_eq!(cleaned, 0);
3002 assert_eq!(sessions.session_count().await, 1);
3003
3004 thread::sleep(Duration::from_millis(5));
3006 let cleaned = NostrServerTransport::cleanup_sessions(
3007 &sessions,
3008 &event_routes,
3009 &request_wrap_kinds,
3010 Duration::from_millis(1),
3011 )
3012 .await;
3013 assert_eq!(cleaned, 1);
3014 assert_eq!(sessions.session_count().await, 0);
3015 assert!(event_routes.pop("evt1").await.is_none());
3016 }
3017
3018 #[tokio::test]
3019 async fn test_cleanup_preserves_active_sessions() {
3020 let sessions = SessionStore::new();
3021 let event_routes = ServerEventRouteStore::new();
3022 let request_wrap_kinds = Arc::new(RwLock::new(HashMap::new()));
3023
3024 sessions
3025 .get_or_create_session("active", false, &event_routes)
3026 .await;
3027
3028 let cleaned = NostrServerTransport::cleanup_sessions(
3029 &sessions,
3030 &event_routes,
3031 &request_wrap_kinds,
3032 Duration::from_secs(300),
3033 )
3034 .await;
3035 assert_eq!(cleaned, 0);
3036 assert_eq!(sessions.session_count().await, 1);
3037 }
3038
3039 #[test]
3042 fn test_pending_request_tracking() {
3043 let mut session = ClientSession::new(false);
3044 session
3045 .pending_requests
3046 .insert("event_abc".to_string(), serde_json::json!(42));
3047 assert_eq!(
3048 session.pending_requests.get("event_abc"),
3049 Some(&serde_json::json!(42))
3050 );
3051 }
3052
3053 #[test]
3054 fn test_progress_token_tracking() {
3055 let mut session = ClientSession::new(false);
3056 session
3057 .event_to_progress_token
3058 .insert("evt1".to_string(), "token1".to_string());
3059 session
3060 .pending_requests
3061 .insert("token1".to_string(), serde_json::json!("evt1"));
3062 assert_eq!(
3063 session.event_to_progress_token.get("evt1"),
3064 Some(&"token1".to_string())
3065 );
3066 }
3067
3068 #[test]
3071 fn test_initialize_always_excluded() {
3072 assert!(NostrServerTransport::is_capability_excluded(
3073 &[],
3074 "initialize",
3075 None
3076 ));
3077 assert!(NostrServerTransport::is_capability_excluded(
3078 &[],
3079 "notifications/initialized",
3080 None
3081 ));
3082 }
3083
3084 #[test]
3085 fn test_method_excluded_without_name() {
3086 let exclusions = vec![CapabilityExclusion {
3087 method: "tools/list".to_string(),
3088 name: None,
3089 }];
3090 assert!(NostrServerTransport::is_capability_excluded(
3091 &exclusions,
3092 "tools/list",
3093 None
3094 ));
3095 assert!(NostrServerTransport::is_capability_excluded(
3096 &exclusions,
3097 "tools/list",
3098 Some("anything")
3099 ));
3100 }
3101
3102 #[test]
3103 fn test_method_excluded_with_name() {
3104 let exclusions = vec![CapabilityExclusion {
3105 method: "tools/call".to_string(),
3106 name: Some("get_weather".to_string()),
3107 }];
3108 assert!(NostrServerTransport::is_capability_excluded(
3109 &exclusions,
3110 "tools/call",
3111 Some("get_weather")
3112 ));
3113 assert!(!NostrServerTransport::is_capability_excluded(
3114 &exclusions,
3115 "tools/call",
3116 Some("other_tool")
3117 ));
3118 assert!(!NostrServerTransport::is_capability_excluded(
3119 &exclusions,
3120 "tools/call",
3121 None
3122 ));
3123 }
3124
3125 #[test]
3126 fn test_non_excluded_method() {
3127 let exclusions = vec![CapabilityExclusion {
3128 method: "tools/list".to_string(),
3129 name: None,
3130 }];
3131 assert!(!NostrServerTransport::is_capability_excluded(
3132 &exclusions,
3133 "tools/call",
3134 None
3135 ));
3136 assert!(!NostrServerTransport::is_capability_excluded(
3137 &exclusions,
3138 "resources/list",
3139 None
3140 ));
3141 }
3142
3143 #[test]
3144 fn test_empty_exclusions_non_init_method() {
3145 assert!(!NostrServerTransport::is_capability_excluded(
3146 &[],
3147 "tools/list",
3148 None
3149 ));
3150 assert!(!NostrServerTransport::is_capability_excluded(
3151 &[],
3152 "tools/call",
3153 Some("x")
3154 ));
3155 }
3156
3157 #[test]
3160 fn test_encryption_mode_default() {
3161 let config = NostrServerTransportConfig::default();
3162 assert_eq!(config.encryption_mode, EncryptionMode::Optional);
3163 }
3164
3165 #[test]
3168 fn test_config_defaults() {
3169 let config = NostrServerTransportConfig::default();
3170 assert_eq!(config.relay_urls, vec!["wss://relay.damus.io".to_string()]);
3171 assert!(!config.is_announced_server);
3172 assert_eq!(config.gift_wrap_mode, GiftWrapMode::Optional);
3173 assert!(config.allowed_public_keys.is_empty());
3174 assert!(config.excluded_capabilities.is_empty());
3175 assert_eq!(config.max_sessions, 1000);
3176 assert_eq!(config.cleanup_interval, Duration::from_secs(60));
3177 assert_eq!(config.session_timeout, Duration::from_secs(300));
3178 assert_eq!(config.request_timeout, Duration::from_secs(60));
3179 assert!(config.server_info.is_none());
3180 assert!(config.relay_list_urls.is_none());
3181 assert!(config.bootstrap_relay_urls.is_none());
3182 assert!(config.publish_relay_list);
3183 assert!(config.profile_metadata.is_none());
3184 }
3185
3186 #[tokio::test]
3187 async fn spawn_discoverability_publication_publishes_kind_0_and_10002_only() {
3188 let pool = Arc::new(MockRelayPool::new());
3189 let relay_pool: Arc<dyn RelayPoolTrait> = pool.clone();
3190 let config = NostrServerTransportConfig::default()
3191 .with_relay_urls(vec!["wss://relay.example.com".to_string()])
3192 .with_profile_metadata(ProfileMetadata::default().with_name("ffi-server"))
3193 .with_publish_relay_list(true);
3194 let mut transport = NostrServerTransport::with_relay_pool(config, relay_pool)
3195 .await
3196 .expect("transport should build");
3197
3198 transport.spawn_discoverability_publication();
3199 for handle in transport.task_handles.drain(..) {
3200 handle.await.expect("discoverability task should not panic");
3201 }
3202
3203 let events = pool.stored_events().await;
3204 assert!(
3205 events.iter().any(|e| e.kind == Kind::Custom(0)),
3206 "profile metadata should be published"
3207 );
3208 assert!(
3209 events
3210 .iter()
3211 .any(|e| e.kind == Kind::Custom(RELAY_LIST_METADATA_KIND)),
3212 "relay list should be published"
3213 );
3214 assert!(
3215 events
3216 .iter()
3217 .all(|e| e.kind != Kind::Custom(SERVER_ANNOUNCEMENT_KIND)),
3218 "direct discoverability publication must not emit CEP-6 announcements"
3219 );
3220 }
3221
3222 #[test]
3225 fn test_select_outbound_gift_wrap_kind_plaintext() {
3226 assert_eq!(
3227 NostrServerTransport::select_outbound_gift_wrap_kind(
3228 GiftWrapMode::Optional,
3229 false,
3230 Some(GIFT_WRAP_KIND),
3231 ),
3232 None
3233 );
3234 }
3235
3236 #[test]
3237 fn test_select_outbound_gift_wrap_kind_mirrors_incoming() {
3238 assert_eq!(
3239 NostrServerTransport::select_outbound_gift_wrap_kind(
3240 GiftWrapMode::Optional,
3241 true,
3242 Some(EPHEMERAL_GIFT_WRAP_KIND),
3243 ),
3244 Some(EPHEMERAL_GIFT_WRAP_KIND)
3245 );
3246 }
3247
3248 #[test]
3249 fn test_select_outbound_gift_wrap_kind_persistent_mode_overrides_ephemeral() {
3250 assert_eq!(
3251 NostrServerTransport::select_outbound_gift_wrap_kind(
3252 GiftWrapMode::Persistent,
3253 true,
3254 Some(EPHEMERAL_GIFT_WRAP_KIND),
3255 ),
3256 Some(GIFT_WRAP_KIND)
3257 );
3258 }
3259
3260 #[test]
3261 fn test_append_common_response_tags_includes_encryption_when_optional() {
3262 let snapshot = announcement_manager::CommonTagsSnapshot {
3263 server_info: None,
3264 extra_common_tags: vec![],
3265 internal_common_tags: vec![],
3266 encryption_mode: EncryptionMode::Optional,
3267 gift_wrap_mode: GiftWrapMode::Optional,
3268 };
3269 let mut tags = Vec::new();
3270 snapshot.append_common_response_tags(&mut tags);
3271 let kinds: Vec<String> = tags.iter().map(|t| format!("{:?}", t.kind())).collect();
3272 assert!(
3273 kinds.iter().any(|k| k.contains("support_encryption")),
3274 "should include support_encryption tag"
3275 );
3276 }
3277
3278 #[test]
3279 fn test_append_common_response_tags_no_encryption_when_disabled() {
3280 let snapshot = announcement_manager::CommonTagsSnapshot {
3281 server_info: None,
3282 extra_common_tags: vec![],
3283 internal_common_tags: vec![],
3284 encryption_mode: EncryptionMode::Disabled,
3285 gift_wrap_mode: GiftWrapMode::Optional,
3286 };
3287 let mut tags = Vec::new();
3288 snapshot.append_common_response_tags(&mut tags);
3289 assert!(
3290 tags.is_empty(),
3291 "should not include encryption tags when encryption disabled"
3292 );
3293 }
3294
3295 #[test]
3296 fn test_select_outbound_notification_gift_wrap_kind_plaintext() {
3297 assert_eq!(
3298 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3299 GiftWrapMode::Optional,
3300 false,
3301 Some(EPHEMERAL_GIFT_WRAP_KIND),
3302 true,
3303 ),
3304 None
3305 );
3306 }
3307
3308 #[test]
3309 fn test_select_outbound_notification_gift_wrap_kind_mirrors_correlated() {
3310 assert_eq!(
3311 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3312 GiftWrapMode::Optional,
3313 true,
3314 Some(EPHEMERAL_GIFT_WRAP_KIND),
3315 false,
3316 ),
3317 Some(EPHEMERAL_GIFT_WRAP_KIND)
3318 );
3319 }
3320
3321 #[test]
3322 fn test_select_outbound_notification_gift_wrap_kind_falls_back_to_mode_if_correlated_not_allowed(
3323 ) {
3324 assert_eq!(
3325 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3326 GiftWrapMode::Ephemeral,
3327 true,
3328 Some(GIFT_WRAP_KIND),
3329 false,
3330 ),
3331 Some(EPHEMERAL_GIFT_WRAP_KIND)
3332 );
3333 }
3334
3335 #[test]
3336 fn test_select_outbound_notification_gift_wrap_kind_uses_ephemeral_if_supported() {
3337 assert_eq!(
3338 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3339 GiftWrapMode::Optional,
3340 true,
3341 None,
3342 true,
3343 ),
3344 Some(EPHEMERAL_GIFT_WRAP_KIND)
3345 );
3346 }
3347
3348 #[test]
3349 fn test_select_outbound_notification_gift_wrap_kind_uses_persistent_if_ephemeral_supported_but_mode_persistent(
3350 ) {
3351 assert_eq!(
3352 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3353 GiftWrapMode::Persistent,
3354 true,
3355 None,
3356 true,
3357 ),
3358 Some(GIFT_WRAP_KIND)
3359 );
3360 }
3361
3362 #[test]
3363 fn test_select_outbound_notification_gift_wrap_kind_uses_default_mode_if_ephemeral_not_supported(
3364 ) {
3365 assert_eq!(
3366 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3367 GiftWrapMode::Optional,
3368 true,
3369 None,
3370 false,
3371 ),
3372 Some(GIFT_WRAP_KIND)
3373 );
3374 }
3375
3376 #[test]
3377 fn test_append_common_response_tags_includes_ephemeral_tag() {
3378 let snapshot = announcement_manager::CommonTagsSnapshot {
3379 server_info: None,
3380 extra_common_tags: vec![],
3381 internal_common_tags: vec![],
3382 encryption_mode: EncryptionMode::Optional,
3383 gift_wrap_mode: GiftWrapMode::Optional,
3384 };
3385 let mut tags = Vec::new();
3386 snapshot.append_common_response_tags(&mut tags);
3387 let kinds: Vec<String> = tags.iter().map(|t| format!("{:?}", t.kind())).collect();
3388 assert!(
3389 kinds
3390 .iter()
3391 .any(|k| k.contains("support_encryption_ephemeral")),
3392 "should include support_encryption_ephemeral tag"
3393 );
3394 }
3395
3396 #[test]
3397 fn test_append_common_response_tags_includes_server_info() {
3398 let server_info = ServerInfo {
3399 name: Some("TestServer".to_string()),
3400 ..Default::default()
3401 };
3402 let snapshot = announcement_manager::CommonTagsSnapshot {
3403 server_info: Some(server_info),
3404 extra_common_tags: vec![],
3405 internal_common_tags: vec![],
3406 encryption_mode: EncryptionMode::Disabled,
3407 gift_wrap_mode: GiftWrapMode::Optional,
3408 };
3409 let mut tags = Vec::new();
3410 snapshot.append_common_response_tags(&mut tags);
3411 let tag_value = tags
3412 .iter()
3413 .find(|t| (*t).clone().to_vec().first().map(|s| s.as_str()) == Some("name"))
3414 .and_then(|t| t.clone().to_vec().get(1).cloned());
3415 assert_eq!(tag_value.as_deref(), Some("TestServer"));
3416 }
3417
3418 #[test]
3419 fn test_append_common_response_tags_extra_tags() {
3420 let extra_tags = vec![Tag::custom(
3421 TagKind::Custom("custom_tag".into()),
3422 vec!["value".to_string()],
3423 )];
3424 let snapshot = announcement_manager::CommonTagsSnapshot {
3425 server_info: None,
3426 extra_common_tags: extra_tags,
3427 internal_common_tags: vec![],
3428 encryption_mode: EncryptionMode::Disabled,
3429 gift_wrap_mode: GiftWrapMode::Optional,
3430 };
3431 let mut tags = Vec::new();
3432 snapshot.append_common_response_tags(&mut tags);
3433 let tag_value = tags
3434 .iter()
3435 .find(|t| (*t).clone().to_vec().first().map(|s| s.as_str()) == Some("custom_tag"))
3436 .and_then(|t| t.clone().to_vec().get(1).cloned());
3437 assert_eq!(tag_value.as_deref(), Some("value"));
3438 }
3439
3440 #[test]
3443 fn test_cep35_client_session_new_fields_default_false() {
3444 let session = ClientSession::new(false);
3445 assert!(!session.has_sent_common_tags);
3446 assert!(!session.supports_encryption);
3447 assert!(!session.supports_ephemeral_encryption);
3448 assert!(!session.supports_oversized_transfer);
3449 }
3450
3451 #[test]
3452 fn test_cep35_capability_or_assign() {
3453 let mut session = ClientSession::new(false);
3454
3455 session.supports_encryption |= true;
3456 session.supports_ephemeral_encryption |= false;
3457
3458 session.supports_encryption |= false;
3459 session.supports_ephemeral_encryption |= true;
3460
3461 assert!(session.supports_encryption, "OR-assign must not downgrade");
3462 assert!(session.supports_ephemeral_encryption);
3463 assert!(!session.supports_oversized_transfer);
3464 }
3465
3466 #[test]
3467 fn test_config_gift_wrap_mode_default() {
3468 let config = NostrServerTransportConfig::default();
3469 assert_eq!(config.gift_wrap_mode, GiftWrapMode::Optional);
3470 }
3471
3472 fn first_tag_values(tags: &[Tag]) -> Vec<String> {
3475 tags.iter().map(|t| t.clone().to_vec()[0].clone()).collect()
3476 }
3477
3478 async fn make_server_with_oversized(enabled: bool) -> NostrServerTransport {
3479 let config = NostrServerTransportConfig {
3480 oversized_transfer: OversizedTransferConfig::default().with_enabled(enabled),
3481 ..Default::default()
3482 };
3483 let pool: Arc<dyn RelayPoolTrait> = Arc::new(crate::relay::mock::MockRelayPool::new());
3484 NostrServerTransport::with_relay_pool(config, pool)
3485 .await
3486 .expect("server transport construction")
3487 }
3488
3489 #[test]
3490 fn test_oversized_enabled_by_default() {
3491 let config = NostrServerTransportConfig::default();
3492 assert!(config.oversized_transfer.enabled);
3493 }
3494
3495 #[test]
3496 fn test_oversized_support_tags_helper() {
3497 let mut config = NostrServerTransportConfig::default().with_oversized_enabled(false);
3499 assert!(oversized_support_tags(&config).is_empty());
3500 config.oversized_transfer.enabled = true;
3501 let names = first_tag_values(&oversized_support_tags(&config));
3502 assert_eq!(names, vec!["support_oversized_transfer"]);
3503 }
3504
3505 #[test]
3506 fn test_oversized_builders() {
3507 let config = NostrServerTransportConfig::default().with_oversized_enabled(true);
3508 assert!(config.oversized_transfer.enabled);
3509 let config = NostrServerTransportConfig::default()
3510 .with_oversized_transfer(OversizedTransferConfig::enabled().with_threshold(123));
3511 assert!(config.oversized_transfer.enabled);
3512 assert_eq!(config.oversized_transfer.threshold, 123);
3513 }
3514
3515 #[tokio::test]
3516 async fn test_announcement_includes_oversized_tag_when_enabled() {
3517 let server = make_server_with_oversized(true).await;
3518 let names = first_tag_values(&server.announcement_manager.get_common_tags());
3519 assert!(
3520 names.contains(&"support_oversized_transfer".to_string()),
3521 "announcement common tags must advertise oversized support when enabled"
3522 );
3523 }
3524
3525 #[tokio::test]
3526 async fn test_announcement_omits_oversized_tag_when_disabled() {
3527 let server = make_server_with_oversized(false).await;
3528 let names = first_tag_values(&server.announcement_manager.get_common_tags());
3529 assert!(
3530 !names.contains(&"support_oversized_transfer".to_string()),
3531 "announcement must not advertise oversized support when disabled"
3532 );
3533 }
3534
3535 #[tokio::test]
3536 async fn test_first_response_snapshot_includes_oversized_tag_when_enabled() {
3537 let server = make_server_with_oversized(true).await;
3538 let snapshot = server.announcement_manager.common_tags_snapshot();
3539 let mut tags = Vec::new();
3540 snapshot.append_common_response_tags(&mut tags);
3541 let names = first_tag_values(&tags);
3542 assert!(
3543 names.contains(&"support_oversized_transfer".to_string()),
3544 "first-response replay must carry the oversized tag when enabled"
3545 );
3546 }
3547
3548 #[tokio::test]
3549 async fn test_first_response_snapshot_omits_oversized_tag_when_disabled() {
3550 let server = make_server_with_oversized(false).await;
3551 let snapshot = server.announcement_manager.common_tags_snapshot();
3552 let mut tags = Vec::new();
3553 snapshot.append_common_response_tags(&mut tags);
3554 let names = first_tag_values(&tags);
3555 assert!(!names.contains(&"support_oversized_transfer".to_string()));
3556 }
3557
3558 #[test]
3559 fn test_server_learns_client_oversized_only_when_enabled() {
3560 let oversized_tag = Tag::custom(
3566 TagKind::Custom(tags::SUPPORT_OVERSIZED_TRANSFER.into()),
3567 Vec::<String>::new(),
3568 );
3569 let discovered = learn_peer_capabilities(&[oversized_tag]);
3570 assert!(discovered.supports_oversized_transfer);
3571
3572 let mut session = ClientSession::new(false);
3574 let oversized_enabled = false;
3575 session.supports_oversized_transfer |=
3576 oversized_enabled && discovered.supports_oversized_transfer;
3577 assert!(!session.supports_oversized_transfer);
3578
3579 let oversized_enabled = true;
3581 session.supports_oversized_transfer |=
3582 oversized_enabled && discovered.supports_oversized_transfer;
3583 assert!(session.supports_oversized_transfer);
3584 }
3585
3586 #[test]
3589 fn test_open_stream_support_tags_helper() {
3590 assert!(open_stream_support_tags(&OpenStreamConfig::default()).is_empty());
3592 let names = first_tag_values(&open_stream_support_tags(&OpenStreamConfig::enabled()));
3593 assert_eq!(names, vec!["support_open_stream"]);
3594 }
3595
3596 #[test]
3597 fn test_internal_common_capability_tags_merges_both() {
3598 let config = NostrServerTransportConfig::default()
3599 .with_oversized_enabled(true)
3600 .with_open_stream(OpenStreamConfig::enabled());
3601 let names = first_tag_values(&internal_common_capability_tags(&config));
3602 assert!(names.contains(&"support_oversized_transfer".to_string()));
3603 assert!(names.contains(&"support_open_stream".to_string()));
3604 }
3605
3606 #[tokio::test]
3607 async fn test_announcement_includes_open_stream_tag_when_enabled() {
3608 let config = NostrServerTransportConfig {
3609 open_stream: OpenStreamConfig::enabled(),
3610 ..Default::default()
3611 };
3612 let pool: Arc<dyn RelayPoolTrait> = Arc::new(crate::relay::mock::MockRelayPool::new());
3613 let server = NostrServerTransport::with_relay_pool(config, pool)
3614 .await
3615 .expect("server transport construction");
3616 let names = first_tag_values(&server.announcement_manager.get_common_tags());
3617 assert!(
3618 names.contains(&"support_open_stream".to_string()),
3619 "announcement must advertise open-stream support when enabled"
3620 );
3621 }
3622
3623 #[tokio::test]
3624 async fn test_announcement_omits_open_stream_tag_when_disabled() {
3625 let pool: Arc<dyn RelayPoolTrait> = Arc::new(crate::relay::mock::MockRelayPool::new());
3627 let server =
3628 NostrServerTransport::with_relay_pool(NostrServerTransportConfig::default(), pool)
3629 .await
3630 .expect("server transport construction");
3631 let names = first_tag_values(&server.announcement_manager.get_common_tags());
3632 assert!(!names.contains(&"support_open_stream".to_string()));
3633 }
3634
3635 #[test]
3636 fn test_server_learns_client_open_stream_only_when_enabled() {
3637 let open_stream_tag = Tag::custom(
3638 TagKind::Custom(tags::SUPPORT_OPEN_STREAM.into()),
3639 Vec::<String>::new(),
3640 );
3641 let discovered = learn_peer_capabilities(&[open_stream_tag]);
3642 assert!(discovered.supports_open_stream);
3643
3644 let mut session = ClientSession::new(false);
3646 let open_stream_enabled = false;
3647 session.supports_open_stream |= open_stream_enabled && discovered.supports_open_stream;
3648 assert!(!session.supports_open_stream);
3649
3650 let open_stream_enabled = true;
3652 session.supports_open_stream |= open_stream_enabled && discovered.supports_open_stream;
3653 assert!(session.supports_open_stream);
3654 }
3655
3656 fn deferral_test_writer(token: &str) -> OpenStreamWriter {
3660 let publish_frame: PublishFrame = Arc::new(|_frame: JsonRpcNotification| {
3661 Box::pin(async move { Ok(EventId::all_zeros()) })
3662 });
3663 OpenStreamWriter::new(OpenStreamWriterOptions {
3664 progress_token: token.to_string(),
3665 publish_frame,
3666 content_type: None,
3667 on_close: None,
3668 on_abort: None,
3669 idle_timeout: None,
3670 probe_timeout: Duration::from_millis(20_000),
3671 })
3672 }
3673
3674 fn install_slot(
3677 state: &ServerOpenStreamState,
3678 event_id: &str,
3679 writer: OpenStreamWriter,
3680 terminated: bool,
3681 ) {
3682 let token = writer.progress_token().to_string();
3683 let snapshot = RouteSnapshot {
3684 client_pubkey: Keys::generate().public_key(),
3685 original_request_id: serde_json::json!(1),
3686 is_encrypted: false,
3687 mirrored_wrap_kind: None,
3688 };
3689 state.lock_slots().insert(
3690 event_id.to_string(),
3691 OpenStreamSlot {
3692 writer,
3693 snapshot,
3694 pending_response: None,
3695 terminated,
3696 },
3697 );
3698 state.lock_token_index().insert(token, event_id.to_string());
3699 }
3700
3701 fn dummy_response() -> JsonRpcMessage {
3702 JsonRpcMessage::Response(JsonRpcResponse {
3703 jsonrpc: "2.0".to_string(),
3704 id: serde_json::json!(1),
3705 result: serde_json::json!({ "ok": true }),
3706 })
3707 }
3708
3709 #[tokio::test]
3710 async fn try_defer_open_stream_response_branch_coverage() {
3711 let config = NostrServerTransportConfig::default()
3712 .with_open_stream(OpenStreamConfig::default().with_enabled(true));
3713 let pool: Arc<dyn RelayPoolTrait> = Arc::new(MockRelayPool::new());
3714 let transport = NostrServerTransport::with_relay_pool(config, pool)
3715 .await
3716 .expect("server transport");
3717
3718 assert!(matches!(
3720 transport.try_defer_open_stream_response("evt-none", dummy_response()),
3721 OpenStreamDeferral::Passthrough(_)
3722 ));
3723
3724 install_slot(
3728 &transport.open_stream,
3729 "evt-unstarted",
3730 deferral_test_writer("tok-unstarted"),
3731 false,
3732 );
3733 assert!(matches!(
3734 transport.try_defer_open_stream_response("evt-unstarted", dummy_response()),
3735 OpenStreamDeferral::Passthrough(_)
3736 ));
3737 assert!(
3738 transport
3739 .open_stream
3740 .lock_slots()
3741 .get("evt-unstarted")
3742 .is_none(),
3743 "unstarted writer slot must be removed (no leak)"
3744 );
3745 assert!(
3746 transport
3747 .open_stream
3748 .lock_token_index()
3749 .get("tok-unstarted")
3750 .is_none(),
3751 "unstarted writer token index must be removed (no leak)"
3752 );
3753
3754 let terminal = deferral_test_writer("tok-terminal");
3758 terminal.start().await.expect("start");
3759 install_slot(&transport.open_stream, "evt-terminal", terminal, true);
3760 assert!(matches!(
3761 transport.try_defer_open_stream_response("evt-terminal", dummy_response()),
3762 OpenStreamDeferral::SendNow { .. }
3763 ));
3764 assert!(transport
3765 .open_stream
3766 .lock_slots()
3767 .get("evt-terminal")
3768 .is_none());
3769 assert!(transport
3770 .open_stream
3771 .lock_token_index()
3772 .get("tok-terminal")
3773 .is_none());
3774
3775 let open = deferral_test_writer("tok-open");
3779 open.start().await.expect("start");
3780 install_slot(&transport.open_stream, "evt-open", open, false);
3781 assert!(matches!(
3782 transport.try_defer_open_stream_response("evt-open", dummy_response()),
3783 OpenStreamDeferral::Deferred
3784 ));
3785 {
3786 let slots = transport.open_stream.lock_slots();
3787 let slot = slots.get("evt-open").expect("deferred slot retained");
3788 assert!(
3789 slot.pending_response.is_some(),
3790 "the deferred response must be stashed for the hook to flush"
3791 );
3792 }
3793
3794 let disabled = NostrServerTransport::with_relay_pool(
3797 NostrServerTransportConfig::default()
3798 .with_open_stream(OpenStreamConfig::default().with_enabled(false)),
3799 Arc::new(MockRelayPool::new()) as Arc<dyn RelayPoolTrait>,
3800 )
3801 .await
3802 .expect("disabled server transport");
3803 install_slot(
3804 &disabled.open_stream,
3805 "evt-disabled",
3806 deferral_test_writer("tok-disabled"),
3807 false,
3808 );
3809 assert!(
3810 disabled.get_open_stream_writer("evt-disabled").is_none(),
3811 "a disabled server must not expose writers (deferral never attempted)"
3812 );
3813 }
3814
3815 fn keepalive_test_writer(token: &str, idle_ms: u64, probe_ms: u64) -> OpenStreamWriter {
3820 let publish_frame: PublishFrame = Arc::new(|_frame: JsonRpcNotification| {
3821 Box::pin(async move { Ok(EventId::all_zeros()) })
3822 });
3823 OpenStreamWriter::new(OpenStreamWriterOptions {
3824 progress_token: token.to_string(),
3825 publish_frame,
3826 content_type: None,
3827 on_close: None,
3828 on_abort: None,
3829 idle_timeout: Some(Duration::from_millis(idle_ms)),
3830 probe_timeout: Duration::from_millis(probe_ms),
3831 })
3832 }
3833
3834 #[tokio::test]
3843 async fn sweep_aborts_writer_and_evicts_session_when_client_goes_silent() {
3844 let config = NostrServerTransportConfig::default()
3845 .with_open_stream(OpenStreamConfig::default().with_enabled(true));
3846 let pool: Arc<dyn RelayPoolTrait> = Arc::new(MockRelayPool::new());
3847 let mut transport = NostrServerTransport::with_relay_pool(config, pool)
3848 .await
3849 .expect("server transport");
3850
3851 let writer = keepalive_test_writer("tok-silent", 40, 60);
3852 writer.start().await.expect("start the stream");
3853 install_slot(&transport.open_stream, "evt-silent", writer.clone(), false);
3854 assert!(writer.is_active(), "writer starts active");
3855
3856 let pubkey_hex = transport
3858 .open_stream
3859 .lock_slots()
3860 .get("evt-silent")
3861 .expect("slot")
3862 .snapshot
3863 .client_pubkey
3864 .to_hex();
3865 let evicted = Arc::new(std::sync::atomic::AtomicBool::new(false));
3866 let evicted_cb = evicted.clone();
3867 transport.sessions.set_eviction_callback(Arc::new(move |_| {
3868 evicted_cb.store(true, std::sync::atomic::Ordering::SeqCst);
3869 }));
3870 transport
3871 .sessions
3872 .get_or_create_session(&pubkey_hex, false, &transport.event_routes)
3873 .await;
3874 assert!(
3875 transport.sessions.get_session(&pubkey_hex).await.is_some(),
3876 "session present before the sweep"
3877 );
3878
3879 let relay_pool = Arc::clone(&transport.base.relay_pool);
3880 let encryption_mode = transport.config.encryption_mode;
3881 let gift_wrap_mode = transport.config.gift_wrap_mode;
3882
3883 tokio::time::sleep(Duration::from_millis(60)).await;
3886 NostrServerTransport::sweep_open_stream_sessions(
3887 &transport.open_stream,
3888 &relay_pool,
3889 encryption_mode,
3890 gift_wrap_mode,
3891 &transport.sessions,
3892 )
3893 .await;
3894 assert!(
3895 writer.is_active(),
3896 "writer must remain active while a probe is in flight"
3897 );
3898 assert!(
3899 transport.sessions.get_session(&pubkey_hex).await.is_some(),
3900 "session must survive a mere probe (client not yet declared dead)"
3901 );
3902
3903 tokio::time::sleep(Duration::from_millis(80)).await;
3906 NostrServerTransport::sweep_open_stream_sessions(
3907 &transport.open_stream,
3908 &relay_pool,
3909 encryption_mode,
3910 gift_wrap_mode,
3911 &transport.sessions,
3912 )
3913 .await;
3914 assert!(
3915 !writer.is_active(),
3916 "writer must abort after a silent client misses the probe deadline"
3917 );
3918 assert!(
3919 transport.sessions.get_session(&pubkey_hex).await.is_none(),
3920 "dead client's session must be evicted on probe timeout"
3921 );
3922 assert!(
3923 evicted.load(std::sync::atomic::Ordering::SeqCst),
3924 "eviction callback must fire on probe-timeout session release"
3925 );
3926 }
3927}