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}
441
442impl NostrServerTransport {
443 pub async fn new<T>(signer: T, config: NostrServerTransportConfig) -> Result<Self>
445 where
446 T: IntoNostrSigner,
447 {
448 let relay_pool: Arc<dyn RelayPoolTrait> =
449 Arc::new(RelayPool::new(signer).await.map_err(|error| {
450 tracing::error!(
451 target: LOG_TARGET,
452 error = %error,
453 "Failed to initialize relay pool for server transport"
454 );
455 error
456 })?);
457 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
458 let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new(
459 NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
460 )));
461
462 tracing::info!(
463 target: LOG_TARGET,
464 relay_count = config.relay_urls.len(),
465 announced = config.is_announced_server,
466 encryption_mode = ?config.encryption_mode,
467 gift_wrap_mode = ?config.gift_wrap_mode,
468 "Created server transport"
469 );
470 let mut announcement_manager = announcement_manager::AnnouncementManager::new(
471 Arc::clone(&relay_pool),
472 config.server_info.clone(),
473 config.encryption_mode,
474 config.gift_wrap_mode,
475 tx.clone(),
476 config.relay_urls.clone(),
477 config.relay_list_urls.clone(),
478 config.bootstrap_relay_urls.clone(),
479 config.publish_relay_list,
480 config.profile_metadata.clone(),
481 );
482 announcement_manager.set_internal_common_tags(internal_common_capability_tags(&config));
485 Ok(Self {
486 announcement_manager,
487 base: BaseTransport {
488 relay_pool,
489 encryption_mode: config.encryption_mode,
490 is_connected: false,
491 },
492 sessions: SessionStore::with_capacity(config.max_sessions),
493 oversized_receiver: new_oversized_receiver_store(config.max_sessions),
494 open_stream: ServerOpenStreamState::new(&config.open_stream, config.max_sessions),
495 config,
496 event_routes: ServerEventRouteStore::new(),
497 request_wrap_kinds: Arc::new(RwLock::new(HashMap::new())),
498 seen_gift_wrap_ids,
499 message_tx: Some(tx),
500 message_rx: Some(rx),
501 cancellation_token: CancellationToken::new(),
502 task_handles: Vec::new(),
503 })
504 }
505
506 pub async fn with_relay_pool(
508 config: NostrServerTransportConfig,
509 relay_pool: Arc<dyn RelayPoolTrait>,
510 ) -> Result<Self> {
511 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
512 let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new(
513 NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
514 )));
515
516 tracing::info!(
517 target: LOG_TARGET,
518 relay_count = config.relay_urls.len(),
519 announced = config.is_announced_server,
520 encryption_mode = ?config.encryption_mode,
521 "Created server transport (with_relay_pool)"
522 );
523 let mut announcement_manager = announcement_manager::AnnouncementManager::new(
524 Arc::clone(&relay_pool),
525 config.server_info.clone(),
526 config.encryption_mode,
527 config.gift_wrap_mode,
528 tx.clone(),
529 config.relay_urls.clone(),
530 config.relay_list_urls.clone(),
531 config.bootstrap_relay_urls.clone(),
532 config.publish_relay_list,
533 config.profile_metadata.clone(),
534 );
535 announcement_manager.set_internal_common_tags(internal_common_capability_tags(&config));
538 Ok(Self {
539 announcement_manager,
540 base: BaseTransport {
541 relay_pool,
542 encryption_mode: config.encryption_mode,
543 is_connected: false,
544 },
545 sessions: SessionStore::with_capacity(config.max_sessions),
546 oversized_receiver: new_oversized_receiver_store(config.max_sessions),
547 open_stream: ServerOpenStreamState::new(&config.open_stream, config.max_sessions),
548 config,
549 request_wrap_kinds: Arc::new(RwLock::new(HashMap::new())),
550 event_routes: ServerEventRouteStore::new(),
551 seen_gift_wrap_ids,
552 message_tx: Some(tx),
553 message_rx: Some(rx),
554 cancellation_token: CancellationToken::new(),
555 task_handles: Vec::new(),
556 })
557 }
558
559 pub async fn start(&mut self) -> Result<()> {
561 self.base
562 .connect(&self.config.relay_urls)
563 .await
564 .map_err(|error| {
565 tracing::error!(
566 target: LOG_TARGET,
567 error = %error,
568 "Failed to connect server transport to relays"
569 );
570 error
571 })?;
572
573 let pubkey = self.base.get_public_key().await.map_err(|error| {
574 tracing::error!(
575 target: LOG_TARGET,
576 error = %error,
577 "Failed to fetch server transport public key"
578 );
579 error
580 })?;
581 tracing::info!(
582 target: LOG_TARGET,
583 pubkey = %pubkey.to_hex(),
584 "Server transport started"
585 );
586
587 self.base
588 .subscribe_for_pubkey(&pubkey)
589 .await
590 .map_err(|error| {
591 tracing::error!(
592 target: LOG_TARGET,
593 error = %error,
594 pubkey = %pubkey.to_hex(),
595 "Failed to subscribe server transport for pubkey"
596 );
597 error
598 })?;
599
600 let relay_pool = Arc::clone(&self.base.relay_pool);
602 let sessions = self.sessions.clone();
603 let event_routes = self.event_routes.clone();
604 let request_wrap_kinds = self.request_wrap_kinds.clone();
605 let tx = self
606 .message_tx
607 .as_ref()
608 .expect("message_tx must exist before start()")
609 .clone();
610 let allowed = self.config.allowed_public_keys.clone();
611 let excluded = self.config.excluded_capabilities.clone();
612 let encryption_mode = self.config.encryption_mode;
613 let gift_wrap_mode = self.config.gift_wrap_mode;
614 let is_announced_server = self.config.is_announced_server;
615 let oversized_enabled = self.config.oversized_transfer.enabled;
616 let oversized_receiver = self.oversized_receiver.clone();
617 let transfer_policy: TransferPolicy = (&self.config.oversized_transfer).into();
618 let common_tags_snapshot = self.announcement_manager.common_tags_snapshot();
619 let seen_gift_wrap_ids = self.seen_gift_wrap_ids.clone();
620 let open_stream = self.open_stream.clone();
621 let event_loop_token = self.cancellation_token.child_token();
622
623 let event_loop_handle = tokio::spawn(async move {
624 Self::event_loop(
625 relay_pool,
626 sessions,
627 event_routes,
628 request_wrap_kinds,
629 tx,
630 allowed,
631 excluded,
632 encryption_mode,
633 gift_wrap_mode,
634 is_announced_server,
635 oversized_enabled,
636 oversized_receiver,
637 transfer_policy,
638 common_tags_snapshot,
639 seen_gift_wrap_ids,
640 open_stream,
641 event_loop_token,
642 )
643 .await;
644 });
645
646 let sessions_cleanup = self.sessions.clone();
648 let event_routes_cleanup = self.event_routes.clone();
649 let request_wrap_kinds_cleanup = self.request_wrap_kinds.clone();
650 let cleanup_interval = self.config.cleanup_interval;
651 let session_timeout = self.config.session_timeout;
652 let request_timeout = self.config.request_timeout;
653 let cleanup_token = self.cancellation_token.child_token();
654
655 let cleanup_handle = tokio::spawn(async move {
656 let mut interval = tokio::time::interval(cleanup_interval);
657 loop {
658 tokio::select! {
659 _ = cleanup_token.cancelled() => {
660 tracing::info!(
661 target: LOG_TARGET,
662 "Server cleanup task cancelled"
663 );
664 break;
665 }
666 _ = interval.tick() => {
667 let cleaned = Self::cleanup_sessions(
668 &sessions_cleanup,
669 &event_routes_cleanup,
670 &request_wrap_kinds_cleanup,
671 session_timeout,
672 )
673 .await;
674 if cleaned > 0 {
675 tracing::info!(
676 target: LOG_TARGET,
677 cleaned_sessions = cleaned,
678 "Cleaned up inactive sessions"
679 );
680 }
681 }
682 }
683
684 let swept_event_ids = event_routes_cleanup
686 .sweep_stale_routes(request_timeout)
687 .await;
688 if !swept_event_ids.is_empty() {
689 let mut kinds_w = request_wrap_kinds_cleanup.write().await;
690 for event_id in &swept_event_ids {
691 kinds_w.remove(event_id);
692 }
693 drop(kinds_w);
694 tracing::warn!(
695 target: LOG_TARGET,
696 swept = swept_event_ids.len(),
697 timeout_secs = request_timeout.as_secs(),
698 "Swept stale event routes (rmcp handles timeout errors)"
699 );
700 }
701 }
702 });
703
704 self.task_handles.push(event_loop_handle);
705 self.task_handles.push(cleanup_handle);
706
707 tracing::info!(
708 target: LOG_TARGET,
709 relay_count = self.config.relay_urls.len(),
710 cleanup_interval_secs = self.config.cleanup_interval.as_secs(),
711 session_timeout_secs = self.config.session_timeout.as_secs(),
712 "Server transport loops spawned"
713 );
714 Ok(())
715 }
716
717 pub async fn close(&mut self) -> Result<()> {
719 self.cancellation_token.cancel();
720 for handle in self.task_handles.drain(..) {
721 let _ = handle.await;
722 }
723 self.announcement_manager.shutdown();
724 self.message_tx.take();
725 self.base.disconnect().await?;
726 self.sessions.clear().await;
727 self.event_routes.clear().await;
728 self.oversized_receiver.write().await.clear();
729 {
732 let mut receivers = self.open_stream.receiver.lock().await;
733 for (_, receiver) in receivers.iter_mut() {
734 receiver.clear();
735 }
736 receivers.clear();
737 }
738 self.open_stream.lock_slots().clear();
739 self.open_stream.lock_token_index().clear();
740 Ok(())
741 }
742
743 pub async fn send_response(&self, event_id: &str, mut response: JsonRpcMessage) -> Result<()> {
745 if self.open_stream.enabled {
749 match self.try_defer_open_stream_response(event_id, response) {
750 OpenStreamDeferral::Deferred => return Ok(()),
752 OpenStreamDeferral::SendNow { snapshot, response } => {
754 return self
755 .send_open_stream_deferred_response(event_id, &snapshot, response)
756 .await;
757 }
758 OpenStreamDeferral::Passthrough(returned) => response = returned,
760 }
761 }
762
763 let route = self.event_routes.pop(event_id).await.ok_or_else(|| {
766 tracing::error!(
767 target: LOG_TARGET,
768 event_id = %event_id,
769 "No client found for response correlation"
770 );
771 Error::Other(format!("No client found for event {event_id}"))
772 })?;
773
774 let client_pubkey_hex = route.client_pubkey;
775 let original_request_id = route.original_request_id;
776 let progress_token = route.progress_token;
777
778 let mut sessions_w = self.sessions.write().await;
779 let session = sessions_w.get_mut(&client_pubkey_hex).ok_or_else(|| {
780 tracing::error!(
781 target: LOG_TARGET,
782 client_pubkey = %client_pubkey_hex,
783 "No session for correlated client"
784 );
785 Error::Other(format!("No session for client {client_pubkey_hex}"))
786 })?;
787
788 match &mut response {
790 JsonRpcMessage::Response(r) => r.id = original_request_id.clone(),
791 JsonRpcMessage::ErrorResponse(r) => r.id = original_request_id.clone(),
792 _ => {}
793 }
794
795 let serialized = serde_json::to_string(&response)?;
799
800 let is_encrypted = session.is_encrypted;
801 let supports_oversized_transfer = session.supports_oversized_transfer;
803
804 let discovery_tags = self.take_pending_server_discovery_tags(session);
806 drop(sessions_w);
807
808 let mirrored_wrap_kind = self
810 .request_wrap_kinds
811 .read()
812 .await
813 .get(event_id)
814 .copied()
815 .flatten();
816
817 let client_pubkey = PublicKey::from_hex(&client_pubkey_hex).map_err(|error| {
818 tracing::error!(
819 target: LOG_TARGET,
820 error = %error,
821 client_pubkey = %client_pubkey_hex,
822 "Invalid client pubkey in session map"
823 );
824 Error::Other(error.to_string())
825 })?;
826
827 let event_id_parsed = EventId::from_hex(event_id).map_err(|error| {
828 tracing::error!(
829 target: LOG_TARGET,
830 error = %error,
831 event_id = %event_id,
832 "Invalid event id while sending response"
833 );
834 Error::Other(error.to_string())
835 })?;
836
837 let base_tags = BaseTransport::create_response_tags(&client_pubkey, &event_id_parsed);
838 let tags = BaseTransport::compose_outbound_tags(&base_tags, &discovery_tags, &[]);
839 let gift_wrap_kind = Self::select_outbound_gift_wrap_kind(
840 self.config.gift_wrap_mode,
841 is_encrypted,
842 mirrored_wrap_kind,
843 );
844
845 let oversized_eligible = self.config.oversized_transfer.enabled
849 && progress_token.is_some()
850 && supports_oversized_transfer;
851 let threshold = self.config.oversized_transfer.threshold;
852
853 let mut reuse_event: Option<Event> = None;
861 let fragment = if !oversized_eligible {
862 false
863 } else if serialized.len() >= threshold {
864 true
865 } else {
866 match self
867 .base
868 .prepare_mcp_message(
869 &response,
870 &client_pubkey,
871 CTXVM_MESSAGES_KIND,
872 tags.clone(),
873 Some(is_encrypted),
874 gift_wrap_kind,
875 )
876 .await
877 {
878 Ok((_id, publishable)) => {
879 let published_len = serde_json::to_string(&publishable)
880 .map(|s| s.len())
881 .unwrap_or(usize::MAX);
882 if published_len > threshold {
883 true
884 } else {
885 reuse_event = Some(publishable);
886 false
887 }
888 }
889 Err(error) => {
892 tracing::debug!(
893 target: LOG_TARGET,
894 error = %error,
895 event_id = %event_id,
896 "Single-event build failed; sending response as oversized transfer"
897 );
898 true
899 }
900 }
901 };
902
903 let send_result: Result<()> = if fragment {
906 self.send_oversized_response(
907 &serialized,
908 progress_token.as_deref().unwrap_or_default(),
909 &client_pubkey,
910 &base_tags,
911 tags,
912 is_encrypted,
913 gift_wrap_kind,
914 )
915 .await
916 } else if let Some(publishable) = reuse_event {
917 self.base
919 .relay_pool
920 .publish_event(&publishable)
921 .await
922 .map(|_| ())
923 } else {
924 self.base
925 .send_mcp_message(
926 &response,
927 &client_pubkey,
928 CTXVM_MESSAGES_KIND,
929 tags,
930 Some(is_encrypted),
931 gift_wrap_kind,
932 )
933 .await
934 .map(|_| ())
935 };
936
937 if let Err(error) = send_result {
938 tracing::error!(
939 target: LOG_TARGET,
940 error = %error,
941 client_pubkey = %client_pubkey_hex,
942 event_id = %event_id,
943 "Failed to publish response message"
944 );
945
946 self.event_routes
948 .register(
949 event_id.to_string(),
950 client_pubkey_hex,
951 original_request_id,
952 progress_token,
953 )
954 .await;
955
956 return Err(error);
957 }
958
959 self.request_wrap_kinds.write().await.remove(event_id);
961
962 let mut sessions = self.sessions.write().await;
963 if let Some(session) = sessions.get_mut(&client_pubkey_hex) {
964 if let Some(token) = progress_token {
966 session.pending_requests.remove(&token);
967 }
968 session.event_to_progress_token.remove(event_id);
969 session.pending_requests.remove(event_id);
970 }
971 drop(sessions);
972
973 tracing::debug!(
974 target: LOG_TARGET,
975 client_pubkey = %client_pubkey_hex,
976 event_id = %event_id,
977 encrypted = is_encrypted,
978 "Sent server response and cleaned correlation state"
979 );
980 Ok(())
981 }
982
983 #[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
987 pub(crate) fn get_open_stream_writer(&self, event_id: &str) -> Option<OpenStreamWriter> {
988 if !self.open_stream.enabled {
989 return None;
990 }
991 self.open_stream.writer_for(event_id)
992 }
993
994 fn try_defer_open_stream_response(
998 &self,
999 event_id: &str,
1000 response: JsonRpcMessage,
1001 ) -> OpenStreamDeferral {
1002 let mut slots = self.open_stream.lock_slots();
1003 let Some(slot) = slots.get_mut(event_id) else {
1004 return OpenStreamDeferral::Passthrough(response);
1005 };
1006
1007 if !slot.writer.has_started() {
1008 let token = slot.writer.progress_token().to_string();
1012 slots.remove(event_id);
1013 drop(slots);
1014 self.open_stream.lock_token_index().remove(&token);
1015 return OpenStreamDeferral::Passthrough(response);
1016 }
1017
1018 if slot.terminated {
1019 let snapshot = slot.snapshot.clone();
1022 let token = slot.writer.progress_token().to_string();
1023 slots.remove(event_id);
1024 drop(slots);
1025 self.open_stream.lock_token_index().remove(&token);
1026 OpenStreamDeferral::SendNow { snapshot, response }
1027 } else {
1028 slot.pending_response = Some(response);
1031 OpenStreamDeferral::Deferred
1032 }
1033 }
1034
1035 async fn send_open_stream_deferred_response(
1038 &self,
1039 event_id: &str,
1040 snapshot: &RouteSnapshot,
1041 response: JsonRpcMessage,
1042 ) -> Result<()> {
1043 Self::publish_open_stream_deferred_response(
1044 &self.base,
1045 self.config.gift_wrap_mode,
1046 event_id,
1047 snapshot,
1048 response,
1049 )
1050 .await
1051 }
1052
1053 async fn publish_open_stream_deferred_response(
1056 base: &BaseTransport,
1057 gift_wrap_mode: GiftWrapMode,
1058 event_id: &str,
1059 snapshot: &RouteSnapshot,
1060 mut response: JsonRpcMessage,
1061 ) -> Result<()> {
1062 match &mut response {
1065 JsonRpcMessage::Response(r) => r.id = snapshot.original_request_id.clone(),
1066 JsonRpcMessage::ErrorResponse(r) => r.id = snapshot.original_request_id.clone(),
1067 _ => {}
1068 }
1069 let event_id_parsed = EventId::from_hex(event_id).map_err(|error| {
1070 Error::Other(format!("Invalid event id for deferred response: {error}"))
1071 })?;
1072 let tags = BaseTransport::create_response_tags(&snapshot.client_pubkey, &event_id_parsed);
1075 let gift_wrap_kind = Self::select_outbound_gift_wrap_kind(
1076 gift_wrap_mode,
1077 snapshot.is_encrypted,
1078 snapshot.mirrored_wrap_kind,
1079 );
1080 base.send_mcp_message(
1081 &response,
1082 &snapshot.client_pubkey,
1083 CTXVM_MESSAGES_KIND,
1084 tags,
1085 Some(snapshot.is_encrypted),
1086 gift_wrap_kind,
1087 )
1088 .await
1089 .map(|_| ())
1090 }
1091
1092 async fn flush_open_stream_response(
1096 state: &ServerOpenStreamState,
1097 base: &BaseTransport,
1098 gift_wrap_mode: GiftWrapMode,
1099 event_id: &str,
1100 ) {
1101 let ready = {
1102 let mut slots = state.lock_slots();
1103 match slots.get_mut(event_id) {
1104 Some(slot) => {
1105 slot.terminated = true;
1106 slot.pending_response.take().map(|response| {
1107 (
1108 slot.snapshot.clone(),
1109 slot.writer.progress_token().to_string(),
1110 response,
1111 )
1112 })
1113 }
1114 None => None,
1115 }
1116 };
1117
1118 let Some((snapshot, token, response)) = ready else {
1119 return;
1122 };
1123
1124 state.lock_slots().remove(event_id);
1127 state.lock_token_index().remove(&token);
1128 if let Err(error) = Self::publish_open_stream_deferred_response(
1129 base,
1130 gift_wrap_mode,
1131 event_id,
1132 &snapshot,
1133 response,
1134 )
1135 .await
1136 {
1137 tracing::error!(
1138 target: LOG_TARGET,
1139 error = %error,
1140 event_id = %event_id,
1141 "Failed to flush deferred open-stream response"
1142 );
1143 }
1144 }
1145
1146 #[allow(clippy::too_many_arguments)]
1156 async fn send_oversized_response(
1157 &self,
1158 serialized: &str,
1159 progress_token: &str,
1160 recipient: &PublicKey,
1161 base_tags: &[Tag],
1162 start_tags: Vec<Tag>,
1163 is_encrypted: bool,
1164 gift_wrap_kind: Option<u16>,
1165 ) -> Result<()> {
1166 let chunk_size = resolve_safe_chunk_size(
1173 self.config.oversized_transfer.chunk_size,
1174 &self.base,
1175 recipient,
1176 base_tags,
1177 is_encrypted,
1178 Kind::Custom(gift_wrap_kind.unwrap_or(GIFT_WRAP_KIND)),
1179 self.config.oversized_transfer.threshold,
1180 )
1181 .await?;
1182 let options = OversizedSenderOptions::new(progress_token).with_chunk_size(chunk_size);
1183 let frames = build_oversized_frames(serialized, &options)?.into_ordered();
1184
1185 let mut start_tags = Some(start_tags);
1188 for frame in frames {
1189 let tags = start_tags.take().unwrap_or_else(|| base_tags.to_vec());
1190 let message = JsonRpcMessage::Notification(frame);
1191 self.base
1192 .send_mcp_message(
1193 &message,
1194 recipient,
1195 CTXVM_MESSAGES_KIND,
1196 tags,
1197 Some(is_encrypted),
1198 gift_wrap_kind,
1199 )
1200 .await?;
1201 }
1202 Ok(())
1203 }
1204
1205 pub async fn send_notification(
1207 &self,
1208 client_pubkey_hex: &str,
1209 notification: &JsonRpcMessage,
1210 correlated_event_id: Option<&str>,
1211 ) -> Result<()> {
1212 let mut sessions = self.sessions.write().await;
1213 let session = sessions
1214 .get_mut(client_pubkey_hex)
1215 .ok_or_else(|| Error::Other(format!("No session for {client_pubkey_hex}")))?;
1216 let is_encrypted = session.is_encrypted;
1217 let supports_ephemeral = session.supports_ephemeral_gift_wrap;
1218
1219 let discovery_tags = self.take_pending_server_discovery_tags(session);
1221 drop(sessions);
1222
1223 let client_pubkey =
1224 PublicKey::from_hex(client_pubkey_hex).map_err(|e| Error::Other(e.to_string()))?;
1225
1226 let mut base_tags = BaseTransport::create_recipient_tags(&client_pubkey);
1227 if let Some(eid) = correlated_event_id {
1228 let event_id = EventId::from_hex(eid).map_err(|e| Error::Other(e.to_string()))?;
1229 base_tags.push(Tag::event(event_id));
1230 }
1231
1232 let tags = BaseTransport::compose_outbound_tags(&base_tags, &discovery_tags, &[]);
1233
1234 let correlated_wrap_kind = if let Some(event_id) = correlated_event_id {
1236 self.request_wrap_kinds
1237 .read()
1238 .await
1239 .get(event_id)
1240 .copied()
1241 .flatten()
1242 } else {
1243 None
1244 };
1245
1246 self.base
1247 .send_mcp_message(
1248 notification,
1249 &client_pubkey,
1250 CTXVM_MESSAGES_KIND,
1251 tags,
1252 Some(is_encrypted),
1253 Self::select_outbound_notification_gift_wrap_kind(
1254 self.config.gift_wrap_mode,
1255 is_encrypted,
1256 correlated_wrap_kind,
1257 supports_ephemeral,
1258 ),
1259 )
1260 .await?;
1261
1262 Ok(())
1263 }
1264
1265 pub async fn broadcast_notification(&self, notification: &JsonRpcMessage) -> Result<()> {
1267 let sessions = self.sessions.read().await;
1268 let initialized: Vec<String> = sessions
1269 .iter()
1270 .filter(|(_, s)| s.is_initialized)
1271 .map(|(k, _)| k.clone())
1272 .collect();
1273 drop(sessions);
1274
1275 for pubkey in initialized {
1276 if let Err(error) = self.send_notification(&pubkey, notification, None).await {
1277 tracing::error!(
1278 target: LOG_TARGET,
1279 error = %error,
1280 client_pubkey = %pubkey,
1281 "Failed to send notification"
1282 );
1283 }
1284 }
1285 Ok(())
1286 }
1287
1288 pub fn take_message_receiver(
1290 &mut self,
1291 ) -> Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>> {
1292 self.message_rx.take()
1293 }
1294
1295 pub async fn session_snapshot(&self, client_pubkey: &str) -> Option<SessionSnapshot> {
1299 self.sessions.get_session(client_pubkey).await
1300 }
1301
1302 pub fn set_announcement_extra_tags(&mut self, tags: Vec<Tag>) {
1304 self.announcement_manager.set_extra_common_tags(tags);
1305 }
1306
1307 pub fn set_announcement_pricing_tags(&mut self, tags: Vec<Tag>) {
1309 self.announcement_manager.set_pricing_tags(tags);
1310 }
1311
1312 pub async fn announce(&self) -> Result<EventId> {
1314 self.announcement_manager.announce().await
1315 }
1316
1317 pub async fn publish_tools(&self, tools: Vec<serde_json::Value>) -> Result<EventId> {
1319 self.announcement_manager.publish_tools(tools).await
1320 }
1321
1322 pub async fn publish_resources(&self, resources: Vec<serde_json::Value>) -> Result<EventId> {
1324 self.announcement_manager.publish_resources(resources).await
1325 }
1326
1327 pub async fn publish_prompts(&self, prompts: Vec<serde_json::Value>) -> Result<EventId> {
1329 self.announcement_manager.publish_prompts(prompts).await
1330 }
1331
1332 pub async fn publish_resource_templates(
1334 &self,
1335 templates: Vec<serde_json::Value>,
1336 ) -> Result<EventId> {
1337 self.announcement_manager
1338 .publish_resource_templates(templates)
1339 .await
1340 }
1341
1342 pub async fn delete_announcements(&self, reason: &str) -> Result<()> {
1344 self.announcement_manager.delete_announcements(reason).await
1345 }
1346
1347 #[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
1353 pub(crate) fn spawn_announcements(&mut self) {
1354 if self.config.is_announced_server {
1355 let handle = self
1356 .announcement_manager
1357 .spawn_publish_public_announcements(self.cancellation_token.child_token());
1358 self.task_handles.push(handle);
1359 }
1360 self.spawn_discoverability_publication();
1361 }
1362
1363 pub fn spawn_discoverability_publication(&mut self) {
1369 let handle = self.announcement_manager.spawn_publish_discoverability();
1370 self.task_handles.push(handle);
1371 }
1372
1373 #[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
1377 pub(crate) async fn handle_announcement_response(
1378 &self,
1379 response: JsonRpcMessage,
1380 ) -> Result<()> {
1381 self.announcement_manager
1382 .handle_announcement_response(response)
1383 .await
1384 }
1385
1386 #[cfg(feature = "rmcp")]
1388 pub async fn publish_tools_typed(&self, tools: Vec<rmcp::model::Tool>) -> Result<EventId> {
1389 self.announcement_manager.publish_tools_typed(tools).await
1390 }
1391
1392 #[cfg(feature = "rmcp")]
1394 pub async fn publish_resources_typed(
1395 &self,
1396 resources: Vec<rmcp::model::Resource>,
1397 ) -> Result<EventId> {
1398 self.announcement_manager
1399 .publish_resources_typed(resources)
1400 .await
1401 }
1402
1403 #[cfg(feature = "rmcp")]
1405 pub async fn publish_prompts_typed(
1406 &self,
1407 prompts: Vec<rmcp::model::Prompt>,
1408 ) -> Result<EventId> {
1409 self.announcement_manager
1410 .publish_prompts_typed(prompts)
1411 .await
1412 }
1413
1414 #[cfg(feature = "rmcp")]
1416 pub async fn publish_resource_templates_typed(
1417 &self,
1418 templates: Vec<rmcp::model::ResourceTemplate>,
1419 ) -> Result<EventId> {
1420 self.announcement_manager
1421 .publish_resource_templates_typed(templates)
1422 .await
1423 }
1424
1425 fn take_pending_server_discovery_tags(&self, session: &mut ClientSession) -> Vec<Tag> {
1429 if session.has_sent_common_tags {
1430 return vec![];
1431 }
1432 session.has_sent_common_tags = true;
1433 self.announcement_manager.get_common_tags()
1434 }
1435
1436 fn is_capability_excluded(
1439 excluded: &[CapabilityExclusion],
1440 method: &str,
1441 name: Option<&str>,
1442 ) -> bool {
1443 if method == "initialize" || method == "notifications/initialized" {
1445 return true;
1446 }
1447
1448 excluded.iter().any(|excl| {
1449 if excl.method != method {
1450 return false;
1451 }
1452 match (&excl.name, name) {
1453 (Some(excl_name), Some(req_name)) => excl_name == req_name,
1454 (None, _) => true, _ => false,
1456 }
1457 })
1458 }
1459
1460 async fn sweep_oversized_receivers(
1466 oversized_receiver: &Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
1467 ) {
1468 let mut receivers = oversized_receiver.write().await;
1469 let mut empty_peers: Vec<String> = Vec::new();
1470 for (peer, receiver) in receivers.iter_mut() {
1471 for token in receiver.remove_expired() {
1472 tracing::warn!(
1473 target: LOG_TARGET,
1474 client_pubkey = %peer,
1475 token = %token,
1476 "Oversized transfer reaped by watchdog"
1477 );
1478 }
1479 if receiver.active_transfer_count() == 0 {
1480 empty_peers.push(peer.clone());
1481 }
1482 }
1483 for peer in empty_peers {
1485 receivers.pop(&peer);
1486 }
1487 }
1488
1489 #[allow(clippy::too_many_arguments)]
1490 async fn event_loop(
1491 relay_pool: Arc<dyn RelayPoolTrait>,
1492 sessions: SessionStore,
1493 event_routes: ServerEventRouteStore,
1494 request_wrap_kinds: Arc<RwLock<HashMap<String, Option<u16>>>>,
1495 tx: tokio::sync::mpsc::UnboundedSender<IncomingRequest>,
1496 allowed_pubkeys: Vec<String>,
1497 excluded_capabilities: Vec<CapabilityExclusion>,
1498 encryption_mode: EncryptionMode,
1499 gift_wrap_mode: GiftWrapMode,
1500 is_announced_server: bool,
1501 oversized_enabled: bool,
1502 oversized_receiver: Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
1503 transfer_policy: TransferPolicy,
1504 common_tags_snapshot: announcement_manager::CommonTagsSnapshot,
1505 seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
1506 open_stream: ServerOpenStreamState,
1507 cancel: CancellationToken,
1508 ) {
1509 let mut notifications = relay_pool.notifications();
1510
1511 let watchdog_enabled = oversized_enabled && transfer_policy.transfer_timeout_ms != 0;
1515 let sweep_interval = (Duration::from_millis(transfer_policy.transfer_timeout_ms) / 2)
1516 .clamp(Duration::from_secs(1), Duration::from_secs(30));
1517 let mut sweep_timer =
1518 tokio::time::interval_at(tokio::time::Instant::now() + sweep_interval, sweep_interval);
1519
1520 let open_stream_sweep_enabled =
1524 open_stream.enabled && open_stream.policy.idle_timeout_ms != 0;
1525 let open_stream_sweep_interval =
1526 (Duration::from_millis(open_stream.policy.idle_timeout_ms) / 2)
1527 .clamp(Duration::from_secs(1), Duration::from_secs(30));
1528 let mut open_stream_sweep_timer = tokio::time::interval_at(
1529 tokio::time::Instant::now() + open_stream_sweep_interval,
1530 open_stream_sweep_interval,
1531 );
1532
1533 loop {
1534 let notification = tokio::select! {
1535 _ = cancel.cancelled() => {
1536 tracing::info!(
1537 target: LOG_TARGET,
1538 "Server event loop cancelled"
1539 );
1540 break;
1541 }
1542 _ = sweep_timer.tick(), if watchdog_enabled => {
1543 Self::sweep_oversized_receivers(&oversized_receiver).await;
1544 continue;
1545 }
1546 _ = open_stream_sweep_timer.tick(), if open_stream_sweep_enabled => {
1547 Self::sweep_open_stream_sessions(
1548 &open_stream,
1549 &relay_pool,
1550 encryption_mode,
1551 gift_wrap_mode,
1552 )
1553 .await;
1554 continue;
1555 }
1556 result = notifications.recv() => {
1557 match result {
1558 Ok(n) => n,
1559 Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
1560 tracing::warn!(
1561 target: LOG_TARGET,
1562 skipped = n,
1563 "Relay broadcast lagged, skipping missed events"
1564 );
1565 continue;
1566 }
1567 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
1568 }
1569 }
1570 };
1571 if let RelayPoolNotification::Event { event, .. } = notification {
1572 let is_gift_wrap = event.kind == Kind::Custom(GIFT_WRAP_KIND)
1573 || event.kind == Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND);
1574 let outer_kind: u16 = event.kind.as_u16();
1575
1576 if is_gift_wrap && !gift_wrap_mode.allows_kind(outer_kind) {
1578 tracing::warn!(
1579 target: LOG_TARGET,
1580 event_id = %event.id.to_hex(),
1581 event_kind = outer_kind,
1582 configured_mode = ?gift_wrap_mode,
1583 "Dropping gift-wrap because it violates gift_wrap_mode policy"
1584 );
1585 continue;
1586 }
1587
1588 let (content, sender_pubkey, event_id, is_encrypted, inner_tags) = if is_gift_wrap {
1589 if encryption_mode == EncryptionMode::Disabled {
1590 tracing::warn!(
1591 target: LOG_TARGET,
1592 event_id = %event.id.to_hex(),
1593 sender_pubkey = %event.pubkey.to_hex(),
1594 "Received encrypted message but encryption is disabled"
1595 );
1596 continue;
1597 }
1598 {
1599 let guard = match seen_gift_wrap_ids.lock() {
1600 Ok(g) => g,
1601 Err(poisoned) => poisoned.into_inner(),
1602 };
1603 if guard.contains(&event.id) {
1604 tracing::debug!(
1605 target: LOG_TARGET,
1606 event_id = %event.id.to_hex(),
1607 "Skipping duplicate gift-wrap (outer id)"
1608 );
1609 continue;
1610 }
1611 }
1612 let signer = match relay_pool.signer().await {
1614 Ok(s) => s,
1615 Err(error) => {
1616 tracing::error!(
1617 target: LOG_TARGET,
1618 error = %error,
1619 "Failed to get signer"
1620 );
1621 continue;
1622 }
1623 };
1624 match encryption::decrypt_gift_wrap_single_layer(&signer, &event).await {
1625 Ok(decrypted_json) => {
1626 match serde_json::from_str::<Event>(&decrypted_json) {
1630 Ok(inner) => {
1631 if let Err(e) = inner.verify() {
1632 tracing::warn!(
1633 "Inner event signature verification failed: {e}"
1634 );
1635 continue;
1636 }
1637 {
1638 let mut guard = match seen_gift_wrap_ids.lock() {
1639 Ok(g) => g,
1640 Err(poisoned) => poisoned.into_inner(),
1641 };
1642 guard.put(event.id, ());
1643 }
1644 let inner_tags: Vec<Tag> = inner.tags.to_vec();
1645 (
1646 inner.content,
1647 inner.pubkey.to_hex(),
1648 inner.id.to_hex(),
1649 true,
1650 inner_tags,
1651 )
1652 }
1653 Err(error) => {
1654 tracing::error!(
1655 target: LOG_TARGET,
1656 error = %error,
1657 "Failed to parse inner event"
1658 );
1659 continue;
1660 }
1661 }
1662 }
1663 Err(error) => {
1664 tracing::error!(
1665 target: LOG_TARGET,
1666 error = %error,
1667 "Failed to decrypt"
1668 );
1669 continue;
1670 }
1671 }
1672 } else {
1673 if encryption_mode == EncryptionMode::Required {
1674 tracing::warn!(
1675 target: LOG_TARGET,
1676 sender_pubkey = %event.pubkey.to_hex(),
1677 "Received unencrypted message but encryption is required"
1678 );
1679 continue;
1680 }
1681 (
1682 event.content.clone(),
1683 event.pubkey.to_hex(),
1684 event.id.to_hex(),
1685 false,
1686 event.tags.to_vec(),
1687 )
1688 };
1689
1690 let mcp_msg = match validation::validate_and_parse(&content) {
1692 Some(msg) => msg,
1693 None => {
1694 tracing::warn!(
1695 target: LOG_TARGET,
1696 sender_pubkey = %sender_pubkey,
1697 "Invalid MCP message"
1698 );
1699 continue;
1700 }
1701 };
1702
1703 if !allowed_pubkeys.is_empty() {
1705 let method = mcp_msg.method().unwrap_or("");
1706 let name = match &mcp_msg {
1707 JsonRpcMessage::Request(r) => r
1708 .params
1709 .as_ref()
1710 .and_then(|p| p.get("name"))
1711 .and_then(|n| n.as_str()),
1712 _ => None,
1713 };
1714
1715 let is_excluded =
1716 Self::is_capability_excluded(&excluded_capabilities, method, name);
1717
1718 if !allowed_pubkeys.contains(&sender_pubkey) && !is_excluded {
1719 tracing::warn!(
1720 target: LOG_TARGET,
1721 sender_pubkey = %sender_pubkey,
1722 method = method,
1723 "Unauthorized request"
1724 );
1725
1726 if is_announced_server {
1729 if let JsonRpcMessage::Request(ref req) = mcp_msg {
1730 if let Ok(client_pk) = PublicKey::from_hex(&sender_pubkey) {
1731 let event_id_parsed = EventId::from_hex(&event_id)
1732 .unwrap_or(EventId::all_zeros());
1733 let mut tags = BaseTransport::create_response_tags(
1734 &client_pk,
1735 &event_id_parsed,
1736 );
1737
1738 let has_sent = sessions
1740 .get_session(&sender_pubkey)
1741 .await
1742 .is_some_and(|s| s.has_sent_common_tags);
1743 if !has_sent {
1744 common_tags_snapshot.append_common_response_tags(&mut tags);
1745 sessions.mark_common_tags_sent(&sender_pubkey).await;
1746 }
1747
1748 let error_response =
1749 JsonRpcMessage::ErrorResponse(JsonRpcErrorResponse {
1750 jsonrpc: "2.0".to_string(),
1751 id: req.id.clone(),
1752 error: JsonRpcError {
1753 code: -32000,
1754 message: "Unauthorized".to_string(),
1755 data: None,
1756 },
1757 });
1758
1759 let base = BaseTransport {
1760 relay_pool: Arc::clone(&relay_pool),
1761 encryption_mode,
1762 is_connected: true,
1763 };
1764 if let Err(e) = base
1765 .send_mcp_message(
1766 &error_response,
1767 &client_pk,
1768 CTXVM_MESSAGES_KIND,
1769 tags,
1770 Some(is_encrypted),
1771 Self::select_outbound_gift_wrap_kind(
1772 gift_wrap_mode,
1773 is_encrypted,
1774 if is_gift_wrap { Some(outer_kind) } else { None },
1775 ),
1776 )
1777 .await
1778 {
1779 tracing::error!(
1780 target: LOG_TARGET,
1781 error = %e,
1782 sender_pubkey = %sender_pubkey,
1783 "Failed to send unauthorized error response"
1784 );
1785 }
1786 }
1787 }
1788 } continue;
1791 }
1792 }
1793
1794 let on_evicted_cb = sessions.eviction_callback();
1796 let mut sessions_w = sessions.write().await;
1797 if !sessions_w.contains(&sender_pubkey) {
1798 let evicted =
1799 sessions_w.push(sender_pubkey.clone(), ClientSession::new(is_encrypted));
1800 SessionStore::handle_eviction(
1801 &sender_pubkey,
1802 evicted,
1803 &mut sessions_w,
1804 on_evicted_cb.as_ref(),
1805 &event_routes,
1806 )
1807 .await;
1808 }
1809 let session = sessions_w.get_mut(&sender_pubkey).unwrap();
1810 session.update_activity();
1811 session.is_encrypted = is_encrypted;
1812
1813 if is_gift_wrap && outer_kind == EPHEMERAL_GIFT_WRAP_KIND {
1815 session.supports_ephemeral_gift_wrap = true;
1816 }
1817
1818 let discovered = learn_peer_capabilities(&inner_tags);
1820 session.supports_encryption |= discovered.supports_encryption;
1821 session.supports_ephemeral_encryption |= discovered.supports_ephemeral_encryption;
1822 let client_already_supported = session.supports_oversized_transfer;
1826 session.supports_oversized_transfer |=
1828 oversized_enabled && discovered.supports_oversized_transfer;
1829 session.supports_open_stream |=
1833 open_stream.enabled && discovered.supports_open_stream;
1834 let client_supports_open_stream = session.supports_open_stream;
1835
1836 if oversized_enabled {
1840 if let JsonRpcMessage::Notification(ref n) = mcp_msg {
1841 if OversizedTransferReceiver::is_oversized_frame(n) {
1842 drop(sessions_w);
1843 Self::handle_oversized_frame(
1844 n,
1845 &sender_pubkey,
1846 &event_id,
1847 is_encrypted,
1848 is_gift_wrap,
1849 outer_kind,
1850 client_already_supported,
1851 &oversized_receiver,
1852 transfer_policy,
1853 &relay_pool,
1854 encryption_mode,
1855 gift_wrap_mode,
1856 &event_routes,
1857 &request_wrap_kinds,
1858 &tx,
1859 &open_stream,
1860 )
1861 .await;
1862 continue;
1863 }
1864 }
1865 }
1866
1867 if open_stream.enabled {
1872 if let JsonRpcMessage::Notification(ref n) = mcp_msg {
1873 if OpenStreamReceiver::is_open_stream_frame(n) {
1874 drop(sessions_w);
1875 Self::handle_open_stream_frame(
1876 &open_stream,
1877 &relay_pool,
1878 encryption_mode,
1879 gift_wrap_mode,
1880 n,
1881 &sender_pubkey,
1882 &event_id,
1883 is_encrypted,
1884 is_gift_wrap,
1885 outer_kind,
1886 client_supports_open_stream,
1887 )
1888 .await;
1889 continue;
1890 }
1891 }
1892 }
1893
1894 if let JsonRpcMessage::Request(ref req) = mcp_msg {
1896 let original_id = req.id.clone();
1897
1898 let progress_token = req
1905 .params
1906 .as_ref()
1907 .and_then(|p| p.get("_meta"))
1908 .and_then(|m| m.get("progressToken"))
1909 .and_then(progress_token_string);
1910
1911 session
1913 .pending_requests
1914 .insert(event_id.clone(), original_id.clone());
1915 if let Some(ref token) = progress_token {
1916 session
1917 .pending_requests
1918 .insert(token.clone(), serde_json::json!(event_id));
1919 session
1920 .event_to_progress_token
1921 .insert(event_id.clone(), token.clone());
1922 }
1923
1924 drop(sessions_w);
1925
1926 {
1928 let mut kinds_w = request_wrap_kinds.write().await;
1929 kinds_w.insert(
1930 event_id.clone(),
1931 if is_gift_wrap { Some(outer_kind) } else { None },
1932 );
1933 }
1934
1935 let writer_request_id = original_id.clone();
1938 let writer_token = progress_token.clone();
1939
1940 event_routes
1941 .register(
1942 event_id.clone(),
1943 sender_pubkey.clone(),
1944 original_id,
1945 progress_token,
1946 )
1947 .await;
1948
1949 if open_stream.enabled && req.method == "tools/call" {
1954 if let Some(token) = writer_token {
1955 Self::create_open_stream_writer(
1956 &open_stream,
1957 &relay_pool,
1958 encryption_mode,
1959 gift_wrap_mode,
1960 &event_id,
1961 &sender_pubkey,
1962 &token,
1963 writer_request_id,
1964 is_encrypted,
1965 if is_gift_wrap { Some(outer_kind) } else { None },
1966 );
1967 }
1968 }
1969 } else {
1970 drop(sessions_w);
1971 }
1972
1973 if let JsonRpcMessage::Notification(ref n) = mcp_msg {
1975 if n.method == "notifications/initialized" {
1976 let mut sessions_w2 = sessions.write().await;
1977 if let Some(session) = sessions_w2.get_mut(&sender_pubkey) {
1978 session.is_initialized = true;
1979 }
1980 }
1981 }
1982
1983 let _ = tx.send(IncomingRequest {
1985 message: mcp_msg,
1986 client_pubkey: sender_pubkey,
1987 event_id,
1988 is_encrypted,
1989 });
1990 }
1991 }
1992 }
1993
1994 #[allow(clippy::too_many_arguments)]
2002 async fn handle_oversized_frame(
2003 frame: &JsonRpcNotification,
2004 sender_pubkey: &str,
2005 event_id: &str,
2006 is_encrypted: bool,
2007 is_gift_wrap: bool,
2008 outer_kind: u16,
2009 client_already_supported: bool,
2010 oversized_receiver: &Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
2011 transfer_policy: TransferPolicy,
2012 relay_pool: &Arc<dyn RelayPoolTrait>,
2013 encryption_mode: EncryptionMode,
2014 gift_wrap_mode: GiftWrapMode,
2015 event_routes: &ServerEventRouteStore,
2016 request_wrap_kinds: &Arc<RwLock<HashMap<String, Option<u16>>>>,
2017 tx: &tokio::sync::mpsc::UnboundedSender<IncomingRequest>,
2018 open_stream: &ServerOpenStreamState,
2019 ) {
2020 let token = frame
2024 .params
2025 .as_ref()
2026 .and_then(|p| p.get("progressToken"))
2027 .and_then(progress_token_string);
2028
2029 let is_start = frame
2031 .params
2032 .as_ref()
2033 .and_then(|p| p.get("cvm"))
2034 .and_then(OversizedFrame::from_cvm_value)
2035 .is_some_and(|f| matches!(f, OversizedFrame::Start { .. }));
2036 let issued_accept = is_start && !client_already_supported && token.is_some();
2037 if issued_accept {
2038 if let Some(ref token) = token {
2039 Self::emit_accept_frame(
2040 token,
2041 sender_pubkey,
2042 event_id,
2043 is_encrypted,
2044 is_gift_wrap,
2045 outer_kind,
2046 relay_pool,
2047 encryption_mode,
2048 gift_wrap_mode,
2049 )
2050 .await;
2051 }
2052 }
2053
2054 let outcome = {
2062 let mut store = oversized_receiver.write().await;
2063 if !store.contains(sender_pubkey) {
2064 store.put(
2065 sender_pubkey.to_string(),
2066 OversizedTransferReceiver::with_policy(transfer_policy),
2067 );
2068 }
2069 let receiver = store.get_mut(sender_pubkey).unwrap();
2070 let outcome = receiver.process_frame(frame);
2071 if issued_accept && matches!(outcome, Ok(None)) {
2072 if let Some(ref token) = token {
2073 if let Ok(accept) = OversizedFrame::Accept.into_progress_notification(
2074 token,
2075 ACCEPT_PROGRESS,
2076 None,
2077 ) {
2078 let _ = receiver.process_frame(&accept);
2079 }
2080 }
2081 }
2082 outcome
2083 };
2084
2085 match outcome {
2086 Ok(None) => {}
2088 Ok(Some(message)) => {
2090 let original_id = message.id().cloned().unwrap_or(serde_json::Value::Null);
2091 let writer_token = match &message {
2096 JsonRpcMessage::Request(req) if req.method == "tools/call" => req
2097 .params
2098 .as_ref()
2099 .and_then(|p| p.get("_meta"))
2100 .and_then(|m| m.get("progressToken"))
2101 .and_then(progress_token_string),
2102 _ => None,
2103 };
2104 let writer_request_id = original_id.clone();
2105 {
2107 let mut kinds_w = request_wrap_kinds.write().await;
2108 kinds_w.insert(
2109 event_id.to_string(),
2110 if is_gift_wrap { Some(outer_kind) } else { None },
2111 );
2112 }
2113 event_routes
2114 .register(
2115 event_id.to_string(),
2116 sender_pubkey.to_string(),
2117 original_id,
2118 token,
2119 )
2120 .await;
2121 if open_stream.enabled {
2122 if let Some(progress_token) = writer_token {
2123 Self::create_open_stream_writer(
2124 open_stream,
2125 relay_pool,
2126 encryption_mode,
2127 gift_wrap_mode,
2128 event_id,
2129 sender_pubkey,
2130 &progress_token,
2131 writer_request_id,
2132 is_encrypted,
2133 if is_gift_wrap { Some(outer_kind) } else { None },
2134 );
2135 }
2136 }
2137 let _ = tx.send(IncomingRequest {
2138 message,
2139 client_pubkey: sender_pubkey.to_string(),
2140 event_id: event_id.to_string(),
2141 is_encrypted,
2142 });
2143 }
2144 Err(error) => {
2146 tracing::warn!(
2147 target: LOG_TARGET,
2148 error = %error,
2149 sender_pubkey = %sender_pubkey,
2150 "Oversized transfer frame rejected; cleaning up locally"
2151 );
2152 }
2153 }
2154 }
2155
2156 #[allow(clippy::too_many_arguments)]
2160 async fn emit_accept_frame(
2161 token: &str,
2162 sender_pubkey: &str,
2163 start_event_id: &str,
2164 is_encrypted: bool,
2165 is_gift_wrap: bool,
2166 outer_kind: u16,
2167 relay_pool: &Arc<dyn RelayPoolTrait>,
2168 encryption_mode: EncryptionMode,
2169 gift_wrap_mode: GiftWrapMode,
2170 ) {
2171 let client_pk = match PublicKey::from_hex(sender_pubkey) {
2172 Ok(pk) => pk,
2173 Err(_) => return,
2174 };
2175 let event_id_parsed = EventId::from_hex(start_event_id).unwrap_or(EventId::all_zeros());
2176 let accept = match OversizedFrame::Accept.into_progress_notification(
2177 token,
2178 ACCEPT_PROGRESS,
2179 Some("oversized request accepted"),
2180 ) {
2181 Ok(n) => JsonRpcMessage::Notification(n),
2182 Err(error) => {
2183 tracing::error!(
2184 target: LOG_TARGET,
2185 error = %error,
2186 "Failed to build oversized-transfer accept frame"
2187 );
2188 return;
2189 }
2190 };
2191 let tags = BaseTransport::create_response_tags(&client_pk, &event_id_parsed);
2192 let base = BaseTransport {
2193 relay_pool: Arc::clone(relay_pool),
2194 encryption_mode,
2195 is_connected: true,
2196 };
2197 if let Err(error) = base
2198 .send_mcp_message(
2199 &accept,
2200 &client_pk,
2201 CTXVM_MESSAGES_KIND,
2202 tags,
2203 Some(is_encrypted),
2204 Self::select_outbound_gift_wrap_kind(
2205 gift_wrap_mode,
2206 is_encrypted,
2207 if is_gift_wrap { Some(outer_kind) } else { None },
2208 ),
2209 )
2210 .await
2211 {
2212 tracing::error!(
2213 target: LOG_TARGET,
2214 error = %error,
2215 sender_pubkey = %sender_pubkey,
2216 "Failed to send oversized-transfer accept frame"
2217 );
2218 }
2219 }
2220
2221 #[allow(clippy::too_many_arguments)]
2227 fn create_open_stream_writer(
2228 state: &ServerOpenStreamState,
2229 relay_pool: &Arc<dyn RelayPoolTrait>,
2230 encryption_mode: EncryptionMode,
2231 gift_wrap_mode: GiftWrapMode,
2232 event_id: &str,
2233 client_pubkey_hex: &str,
2234 progress_token: &str,
2235 original_request_id: serde_json::Value,
2236 is_encrypted: bool,
2237 mirrored_wrap_kind: Option<u16>,
2238 ) {
2239 let client_pubkey = match PublicKey::from_hex(client_pubkey_hex) {
2240 Ok(pk) => pk,
2241 Err(_) => return,
2242 };
2243 let event_id_parsed = match EventId::from_hex(event_id) {
2244 Ok(id) => id,
2245 Err(_) => return,
2246 };
2247 let gift_wrap_kind =
2248 Self::select_outbound_gift_wrap_kind(gift_wrap_mode, is_encrypted, mirrored_wrap_kind);
2249
2250 let publish_relay_pool = Arc::clone(relay_pool);
2256 let publish_frame: PublishFrame = Arc::new(move |notification: JsonRpcNotification| {
2257 let relay_pool = Arc::clone(&publish_relay_pool);
2258 Box::pin(async move {
2259 let base = BaseTransport {
2260 relay_pool,
2261 encryption_mode,
2262 is_connected: true,
2263 };
2264 let tags = BaseTransport::create_response_tags(&client_pubkey, &event_id_parsed);
2265 let message = JsonRpcMessage::Notification(notification);
2266 base.send_mcp_message(
2267 &message,
2268 &client_pubkey,
2269 CTXVM_MESSAGES_KIND,
2270 tags,
2271 Some(is_encrypted),
2272 gift_wrap_kind,
2273 )
2274 .await
2275 })
2276 });
2277
2278 let on_close: OnCloseHook = {
2280 let state = state.clone();
2281 let relay_pool = Arc::clone(relay_pool);
2282 let event_id = event_id.to_string();
2283 Arc::new(move || {
2284 let state = state.clone();
2285 let relay_pool = Arc::clone(&relay_pool);
2286 let event_id = event_id.clone();
2287 Box::pin(async move {
2288 let base = BaseTransport {
2289 relay_pool,
2290 encryption_mode,
2291 is_connected: true,
2292 };
2293 Self::flush_open_stream_response(&state, &base, gift_wrap_mode, &event_id)
2294 .await;
2295 })
2296 })
2297 };
2298 let on_abort: OnAbortHook = {
2299 let state = state.clone();
2300 let relay_pool = Arc::clone(relay_pool);
2301 let event_id = event_id.to_string();
2302 Arc::new(move |_reason| {
2303 let state = state.clone();
2304 let relay_pool = Arc::clone(&relay_pool);
2305 let event_id = event_id.clone();
2306 Box::pin(async move {
2307 let base = BaseTransport {
2308 relay_pool,
2309 encryption_mode,
2310 is_connected: true,
2311 };
2312 Self::flush_open_stream_response(&state, &base, gift_wrap_mode, &event_id)
2313 .await;
2314 })
2315 })
2316 };
2317
2318 let writer = OpenStreamWriter::new(OpenStreamWriterOptions {
2319 progress_token: progress_token.to_string(),
2320 publish_frame,
2321 content_type: None,
2322 on_close: Some(on_close),
2323 on_abort: Some(on_abort),
2324 });
2325 let snapshot = RouteSnapshot {
2326 client_pubkey,
2327 original_request_id,
2328 is_encrypted,
2329 mirrored_wrap_kind,
2330 };
2331 state.lock_slots().insert(
2332 event_id.to_string(),
2333 OpenStreamSlot {
2334 writer,
2335 snapshot,
2336 pending_response: None,
2337 terminated: false,
2338 },
2339 );
2340 state
2341 .lock_token_index()
2342 .insert(progress_token.to_string(), event_id.to_string());
2343 }
2344
2345 #[allow(clippy::too_many_arguments)]
2349 async fn handle_open_stream_frame(
2350 state: &ServerOpenStreamState,
2351 relay_pool: &Arc<dyn RelayPoolTrait>,
2352 encryption_mode: EncryptionMode,
2353 gift_wrap_mode: GiftWrapMode,
2354 notification: &JsonRpcNotification,
2355 sender_pubkey: &str,
2356 event_id: &str,
2357 is_encrypted: bool,
2358 is_gift_wrap: bool,
2359 outer_kind: u16,
2360 client_supports_open_stream: bool,
2361 ) {
2362 let token = notification
2363 .params
2364 .as_ref()
2365 .and_then(|p| p.get("progressToken"))
2366 .and_then(progress_token_string);
2367 let writer = token
2369 .as_deref()
2370 .and_then(|t| state.event_id_for_token(t))
2371 .and_then(|eid| state.writer_for(&eid));
2372
2373 match open_stream_frame_from_notification(notification) {
2374 Some(OpenStreamFrame::Ping { nonce }) => {
2375 if let Some(writer) = writer {
2376 let _ = writer.pong(nonce).await;
2377 } else {
2378 Self::feed_open_stream_reader(
2379 state,
2380 relay_pool,
2381 encryption_mode,
2382 gift_wrap_mode,
2383 notification,
2384 sender_pubkey,
2385 event_id,
2386 is_encrypted,
2387 is_gift_wrap,
2388 outer_kind,
2389 )
2390 .await;
2391 }
2392 }
2393 Some(OpenStreamFrame::Abort { reason }) => {
2394 if let Some(writer) = writer {
2395 let _ = writer.abort(reason).await;
2396 } else {
2397 Self::feed_open_stream_reader(
2398 state,
2399 relay_pool,
2400 encryption_mode,
2401 gift_wrap_mode,
2402 notification,
2403 sender_pubkey,
2404 event_id,
2405 is_encrypted,
2406 is_gift_wrap,
2407 outer_kind,
2408 )
2409 .await;
2410 }
2411 }
2412 Some(OpenStreamFrame::Start { .. }) => {
2413 Self::feed_open_stream_reader(
2414 state,
2415 relay_pool,
2416 encryption_mode,
2417 gift_wrap_mode,
2418 notification,
2419 sender_pubkey,
2420 event_id,
2421 is_encrypted,
2422 is_gift_wrap,
2423 outer_kind,
2424 )
2425 .await;
2426 if client_supports_open_stream {
2428 if let Some(token) = token.as_deref() {
2429 Self::publish_open_stream_control_frame(
2430 state,
2431 relay_pool,
2432 encryption_mode,
2433 gift_wrap_mode,
2434 OpenStreamFrame::Accept,
2435 token,
2436 sender_pubkey,
2437 Some(event_id),
2438 is_encrypted,
2439 is_gift_wrap,
2440 outer_kind,
2441 )
2442 .await;
2443 }
2444 }
2445 }
2446 _ => {
2448 Self::feed_open_stream_reader(
2449 state,
2450 relay_pool,
2451 encryption_mode,
2452 gift_wrap_mode,
2453 notification,
2454 sender_pubkey,
2455 event_id,
2456 is_encrypted,
2457 is_gift_wrap,
2458 outer_kind,
2459 )
2460 .await;
2461 }
2462 }
2463 }
2464
2465 #[allow(clippy::too_many_arguments)]
2468 async fn feed_open_stream_reader(
2469 state: &ServerOpenStreamState,
2470 relay_pool: &Arc<dyn RelayPoolTrait>,
2471 encryption_mode: EncryptionMode,
2472 gift_wrap_mode: GiftWrapMode,
2473 notification: &JsonRpcNotification,
2474 sender_pubkey: &str,
2475 event_id: &str,
2476 is_encrypted: bool,
2477 is_gift_wrap: bool,
2478 outer_kind: u16,
2479 ) {
2480 let outcome = {
2481 let mut store = state.receiver.lock().await;
2482 if !store.contains(sender_pubkey) {
2483 store.put(
2484 sender_pubkey.to_string(),
2485 OpenStreamReceiver::with_policy(state.policy),
2486 );
2487 }
2488 let receiver = store
2489 .get_mut(sender_pubkey)
2490 .expect("open-stream receiver present after insert");
2491 receiver.process_frame(notification).await
2492 };
2493 match outcome {
2494 Ok(FrameOutcome::SendPong(nonce)) => {
2495 if let Some(token) = notification
2496 .params
2497 .as_ref()
2498 .and_then(|p| p.get("progressToken"))
2499 .and_then(progress_token_string)
2500 {
2501 Self::publish_open_stream_control_frame(
2502 state,
2503 relay_pool,
2504 encryption_mode,
2505 gift_wrap_mode,
2506 OpenStreamFrame::Pong { nonce },
2507 &token,
2508 sender_pubkey,
2509 Some(event_id),
2510 is_encrypted,
2511 is_gift_wrap,
2512 outer_kind,
2513 )
2514 .await;
2515 }
2516 }
2517 Ok(_) => {}
2518 Err(error) => {
2519 tracing::warn!(
2520 target: LOG_TARGET,
2521 error = %error,
2522 sender_pubkey = %sender_pubkey,
2523 "Inbound open-stream frame rejected by server reader engine"
2524 );
2525 }
2526 }
2527 }
2528
2529 #[allow(clippy::too_many_arguments)]
2533 async fn publish_open_stream_control_frame(
2534 state: &ServerOpenStreamState,
2535 relay_pool: &Arc<dyn RelayPoolTrait>,
2536 encryption_mode: EncryptionMode,
2537 gift_wrap_mode: GiftWrapMode,
2538 frame: OpenStreamFrame,
2539 token: &str,
2540 recipient_pubkey: &str,
2541 correlated_event_id: Option<&str>,
2542 is_encrypted: bool,
2543 is_gift_wrap: bool,
2544 outer_kind: u16,
2545 ) {
2546 let recipient = match PublicKey::from_hex(recipient_pubkey) {
2547 Ok(pk) => pk,
2548 Err(_) => return,
2549 };
2550 let progress = state.next_control_progress();
2551 let notification = match frame.into_progress_notification(token, progress, None) {
2552 Ok(n) => n,
2553 Err(error) => {
2554 tracing::error!(
2555 target: LOG_TARGET,
2556 error = %error,
2557 "Failed to build open-stream control frame"
2558 );
2559 return;
2560 }
2561 };
2562 let mut tags = BaseTransport::create_recipient_tags(&recipient);
2563 if let Some(eid) = correlated_event_id.and_then(|id| EventId::from_hex(id).ok()) {
2567 tags.push(Tag::event(eid));
2568 }
2569 let base = BaseTransport {
2570 relay_pool: Arc::clone(relay_pool),
2571 encryption_mode,
2572 is_connected: true,
2573 };
2574 let gift_wrap_kind = Self::select_outbound_gift_wrap_kind(
2575 gift_wrap_mode,
2576 is_encrypted,
2577 if is_gift_wrap { Some(outer_kind) } else { None },
2578 );
2579 if let Err(error) = base
2580 .send_mcp_message(
2581 &JsonRpcMessage::Notification(notification),
2582 &recipient,
2583 CTXVM_MESSAGES_KIND,
2584 tags,
2585 Some(is_encrypted),
2586 gift_wrap_kind,
2587 )
2588 .await
2589 {
2590 tracing::warn!(
2591 target: LOG_TARGET,
2592 error = %error,
2593 "Failed to publish open-stream control frame"
2594 );
2595 }
2596 }
2597
2598 async fn sweep_open_stream_sessions(
2603 state: &ServerOpenStreamState,
2604 relay_pool: &Arc<dyn RelayPoolTrait>,
2605 encryption_mode: EncryptionMode,
2606 gift_wrap_mode: GiftWrapMode,
2607 ) {
2608 let now = Instant::now();
2609 let mut actions: Vec<(String, String, KeepaliveAction)> = Vec::new();
2610 {
2611 let mut store = state.receiver.lock().await;
2612 let mut empty_peers = Vec::new();
2613 for (peer, receiver) in store.iter_mut() {
2614 for (token, action) in receiver.registry_mut().tick_all(now) {
2615 actions.push((peer.clone(), token, action));
2616 }
2617 if receiver.active_stream_count() == 0 {
2618 empty_peers.push(peer.clone());
2619 }
2620 }
2621 for peer in empty_peers {
2622 store.pop(&peer);
2623 }
2624 }
2625
2626 let probe_is_encrypted = encryption_mode != EncryptionMode::Disabled;
2627 for (peer, token, action) in actions {
2628 match action {
2629 KeepaliveAction::SendPing(nonce) => {
2630 let correlated = state.event_id_for_token(&token);
2635 Self::publish_open_stream_control_frame(
2636 state,
2637 relay_pool,
2638 encryption_mode,
2639 gift_wrap_mode,
2640 OpenStreamFrame::Ping { nonce },
2641 &token,
2642 &peer,
2643 correlated.as_deref(),
2644 probe_is_encrypted,
2645 false,
2646 0,
2647 )
2648 .await;
2649 }
2650 KeepaliveAction::Abort(reason) => {
2651 if let Some(eid) = state.event_id_for_token(&token) {
2652 if let Some(writer) = state.writer_for(&eid) {
2653 let _ = writer.abort(Some(reason)).await;
2654 }
2655 }
2656 }
2657 KeepaliveAction::None => {}
2658 }
2659 }
2660 }
2661
2662 async fn cleanup_sessions(
2663 sessions: &SessionStore,
2664 event_routes: &ServerEventRouteStore,
2665 request_wrap_kinds: &Arc<RwLock<HashMap<String, Option<u16>>>>,
2666 timeout: Duration,
2667 ) -> usize {
2668 let mut sessions_w = sessions.write().await;
2669 let mut cleaned = 0;
2670 let mut stale_event_ids = Vec::new();
2671
2672 let expired_keys: Vec<String> = sessions_w
2674 .iter()
2675 .filter(|(_, session)| session.last_activity.elapsed() > timeout)
2676 .map(|(k, _)| k.clone())
2677 .collect();
2678
2679 for key in &expired_keys {
2680 if let Some(session) = sessions_w.pop(key) {
2681 stale_event_ids.extend(session.pending_requests.keys().cloned());
2682 stale_event_ids.extend(session.event_to_progress_token.keys().cloned());
2683 tracing::debug!(
2684 target: LOG_TARGET,
2685 client_pubkey = %key,
2686 "Session expired"
2687 );
2688 cleaned += 1;
2689 }
2690 }
2691 drop(sessions_w);
2692
2693 {
2694 let mut kinds_w = request_wrap_kinds.write().await;
2695 for event_id in &stale_event_ids {
2696 kinds_w.remove(event_id);
2697 }
2698 }
2699
2700 for event_id in &stale_event_ids {
2701 event_routes.pop(event_id).await;
2702 }
2703
2704 cleaned
2705 }
2706
2707 fn select_outbound_gift_wrap_kind(
2711 mode: GiftWrapMode,
2712 is_encrypted: bool,
2713 mirrored_kind: Option<u16>,
2714 ) -> Option<u16> {
2715 if !is_encrypted {
2716 return None;
2717 }
2718 if let Some(kind) = mirrored_kind {
2719 if mode.allows_kind(kind) {
2720 return Some(kind);
2721 }
2722 }
2723 match mode {
2724 GiftWrapMode::Persistent => Some(GIFT_WRAP_KIND),
2725 GiftWrapMode::Ephemeral => Some(EPHEMERAL_GIFT_WRAP_KIND),
2726 GiftWrapMode::Optional => Some(GIFT_WRAP_KIND),
2727 }
2728 }
2729
2730 fn select_outbound_notification_gift_wrap_kind(
2732 mode: GiftWrapMode,
2733 is_encrypted: bool,
2734 correlated_wrap_kind: Option<u16>,
2735 client_supports_ephemeral: bool,
2736 ) -> Option<u16> {
2737 if !is_encrypted {
2738 return None;
2739 }
2740 if let Some(kind) = correlated_wrap_kind {
2742 if mode.allows_kind(kind) {
2743 return Some(kind);
2744 }
2745 }
2746 if client_supports_ephemeral && mode.supports_ephemeral() {
2748 return Some(EPHEMERAL_GIFT_WRAP_KIND);
2749 }
2750 match mode {
2751 GiftWrapMode::Persistent => Some(GIFT_WRAP_KIND),
2752 GiftWrapMode::Ephemeral => Some(EPHEMERAL_GIFT_WRAP_KIND),
2753 GiftWrapMode::Optional => Some(GIFT_WRAP_KIND),
2754 }
2755 }
2756}
2757
2758#[cfg(test)]
2759mod tests {
2760 use super::*;
2761 use crate::relay::mock::MockRelayPool;
2762 use std::thread;
2763
2764 #[test]
2767 fn test_client_session_creation() {
2768 let session = ClientSession::new(true);
2769 assert!(!session.is_initialized);
2770 assert!(session.is_encrypted);
2771 assert!(!session.has_sent_common_tags);
2772 assert!(!session.supports_ephemeral_gift_wrap);
2773 assert!(session.pending_requests.is_empty());
2774 assert!(session.event_to_progress_token.is_empty());
2775 }
2776
2777 #[test]
2778 fn test_client_session_update_activity() {
2779 let mut session = ClientSession::new(false);
2780 let first = session.last_activity;
2781 thread::sleep(Duration::from_millis(10));
2782 session.update_activity();
2783 assert!(session.last_activity > first);
2784 }
2785
2786 #[tokio::test]
2787 async fn test_cleanup_sessions_removes_expired() {
2788 let sessions = SessionStore::new();
2789 let event_routes = ServerEventRouteStore::new();
2790
2791 let mut session = ClientSession::new(false);
2793 session
2794 .pending_requests
2795 .insert("evt1".to_string(), serde_json::json!(1));
2796 sessions.write().await.put("pubkey1".to_string(), session);
2797 event_routes
2798 .register(
2799 "evt1".to_string(),
2800 "pubkey1".to_string(),
2801 serde_json::json!(1),
2802 None,
2803 )
2804 .await;
2805
2806 let request_wrap_kinds = Arc::new(RwLock::new(HashMap::new()));
2807
2808 let cleaned = NostrServerTransport::cleanup_sessions(
2810 &sessions,
2811 &event_routes,
2812 &request_wrap_kinds,
2813 Duration::from_secs(300),
2814 )
2815 .await;
2816 assert_eq!(cleaned, 0);
2817 assert_eq!(sessions.session_count().await, 1);
2818
2819 thread::sleep(Duration::from_millis(5));
2821 let cleaned = NostrServerTransport::cleanup_sessions(
2822 &sessions,
2823 &event_routes,
2824 &request_wrap_kinds,
2825 Duration::from_millis(1),
2826 )
2827 .await;
2828 assert_eq!(cleaned, 1);
2829 assert_eq!(sessions.session_count().await, 0);
2830 assert!(event_routes.pop("evt1").await.is_none());
2831 }
2832
2833 #[tokio::test]
2834 async fn test_cleanup_preserves_active_sessions() {
2835 let sessions = SessionStore::new();
2836 let event_routes = ServerEventRouteStore::new();
2837 let request_wrap_kinds = Arc::new(RwLock::new(HashMap::new()));
2838
2839 sessions
2840 .get_or_create_session("active", false, &event_routes)
2841 .await;
2842
2843 let cleaned = NostrServerTransport::cleanup_sessions(
2844 &sessions,
2845 &event_routes,
2846 &request_wrap_kinds,
2847 Duration::from_secs(300),
2848 )
2849 .await;
2850 assert_eq!(cleaned, 0);
2851 assert_eq!(sessions.session_count().await, 1);
2852 }
2853
2854 #[test]
2857 fn test_pending_request_tracking() {
2858 let mut session = ClientSession::new(false);
2859 session
2860 .pending_requests
2861 .insert("event_abc".to_string(), serde_json::json!(42));
2862 assert_eq!(
2863 session.pending_requests.get("event_abc"),
2864 Some(&serde_json::json!(42))
2865 );
2866 }
2867
2868 #[test]
2869 fn test_progress_token_tracking() {
2870 let mut session = ClientSession::new(false);
2871 session
2872 .event_to_progress_token
2873 .insert("evt1".to_string(), "token1".to_string());
2874 session
2875 .pending_requests
2876 .insert("token1".to_string(), serde_json::json!("evt1"));
2877 assert_eq!(
2878 session.event_to_progress_token.get("evt1"),
2879 Some(&"token1".to_string())
2880 );
2881 }
2882
2883 #[test]
2886 fn test_initialize_always_excluded() {
2887 assert!(NostrServerTransport::is_capability_excluded(
2888 &[],
2889 "initialize",
2890 None
2891 ));
2892 assert!(NostrServerTransport::is_capability_excluded(
2893 &[],
2894 "notifications/initialized",
2895 None
2896 ));
2897 }
2898
2899 #[test]
2900 fn test_method_excluded_without_name() {
2901 let exclusions = vec![CapabilityExclusion {
2902 method: "tools/list".to_string(),
2903 name: None,
2904 }];
2905 assert!(NostrServerTransport::is_capability_excluded(
2906 &exclusions,
2907 "tools/list",
2908 None
2909 ));
2910 assert!(NostrServerTransport::is_capability_excluded(
2911 &exclusions,
2912 "tools/list",
2913 Some("anything")
2914 ));
2915 }
2916
2917 #[test]
2918 fn test_method_excluded_with_name() {
2919 let exclusions = vec![CapabilityExclusion {
2920 method: "tools/call".to_string(),
2921 name: Some("get_weather".to_string()),
2922 }];
2923 assert!(NostrServerTransport::is_capability_excluded(
2924 &exclusions,
2925 "tools/call",
2926 Some("get_weather")
2927 ));
2928 assert!(!NostrServerTransport::is_capability_excluded(
2929 &exclusions,
2930 "tools/call",
2931 Some("other_tool")
2932 ));
2933 assert!(!NostrServerTransport::is_capability_excluded(
2934 &exclusions,
2935 "tools/call",
2936 None
2937 ));
2938 }
2939
2940 #[test]
2941 fn test_non_excluded_method() {
2942 let exclusions = vec![CapabilityExclusion {
2943 method: "tools/list".to_string(),
2944 name: None,
2945 }];
2946 assert!(!NostrServerTransport::is_capability_excluded(
2947 &exclusions,
2948 "tools/call",
2949 None
2950 ));
2951 assert!(!NostrServerTransport::is_capability_excluded(
2952 &exclusions,
2953 "resources/list",
2954 None
2955 ));
2956 }
2957
2958 #[test]
2959 fn test_empty_exclusions_non_init_method() {
2960 assert!(!NostrServerTransport::is_capability_excluded(
2961 &[],
2962 "tools/list",
2963 None
2964 ));
2965 assert!(!NostrServerTransport::is_capability_excluded(
2966 &[],
2967 "tools/call",
2968 Some("x")
2969 ));
2970 }
2971
2972 #[test]
2975 fn test_encryption_mode_default() {
2976 let config = NostrServerTransportConfig::default();
2977 assert_eq!(config.encryption_mode, EncryptionMode::Optional);
2978 }
2979
2980 #[test]
2983 fn test_config_defaults() {
2984 let config = NostrServerTransportConfig::default();
2985 assert_eq!(config.relay_urls, vec!["wss://relay.damus.io".to_string()]);
2986 assert!(!config.is_announced_server);
2987 assert_eq!(config.gift_wrap_mode, GiftWrapMode::Optional);
2988 assert!(config.allowed_public_keys.is_empty());
2989 assert!(config.excluded_capabilities.is_empty());
2990 assert_eq!(config.max_sessions, 1000);
2991 assert_eq!(config.cleanup_interval, Duration::from_secs(60));
2992 assert_eq!(config.session_timeout, Duration::from_secs(300));
2993 assert_eq!(config.request_timeout, Duration::from_secs(60));
2994 assert!(config.server_info.is_none());
2995 assert!(config.relay_list_urls.is_none());
2996 assert!(config.bootstrap_relay_urls.is_none());
2997 assert!(config.publish_relay_list);
2998 assert!(config.profile_metadata.is_none());
2999 }
3000
3001 #[tokio::test]
3002 async fn spawn_discoverability_publication_publishes_kind_0_and_10002_only() {
3003 let pool = Arc::new(MockRelayPool::new());
3004 let relay_pool: Arc<dyn RelayPoolTrait> = pool.clone();
3005 let config = NostrServerTransportConfig::default()
3006 .with_relay_urls(vec!["wss://relay.example.com".to_string()])
3007 .with_profile_metadata(ProfileMetadata::default().with_name("ffi-server"))
3008 .with_publish_relay_list(true);
3009 let mut transport = NostrServerTransport::with_relay_pool(config, relay_pool)
3010 .await
3011 .expect("transport should build");
3012
3013 transport.spawn_discoverability_publication();
3014 for handle in transport.task_handles.drain(..) {
3015 handle.await.expect("discoverability task should not panic");
3016 }
3017
3018 let events = pool.stored_events().await;
3019 assert!(
3020 events.iter().any(|e| e.kind == Kind::Custom(0)),
3021 "profile metadata should be published"
3022 );
3023 assert!(
3024 events
3025 .iter()
3026 .any(|e| e.kind == Kind::Custom(RELAY_LIST_METADATA_KIND)),
3027 "relay list should be published"
3028 );
3029 assert!(
3030 events
3031 .iter()
3032 .all(|e| e.kind != Kind::Custom(SERVER_ANNOUNCEMENT_KIND)),
3033 "direct discoverability publication must not emit CEP-6 announcements"
3034 );
3035 }
3036
3037 #[test]
3040 fn test_select_outbound_gift_wrap_kind_plaintext() {
3041 assert_eq!(
3042 NostrServerTransport::select_outbound_gift_wrap_kind(
3043 GiftWrapMode::Optional,
3044 false,
3045 Some(GIFT_WRAP_KIND),
3046 ),
3047 None
3048 );
3049 }
3050
3051 #[test]
3052 fn test_select_outbound_gift_wrap_kind_mirrors_incoming() {
3053 assert_eq!(
3054 NostrServerTransport::select_outbound_gift_wrap_kind(
3055 GiftWrapMode::Optional,
3056 true,
3057 Some(EPHEMERAL_GIFT_WRAP_KIND),
3058 ),
3059 Some(EPHEMERAL_GIFT_WRAP_KIND)
3060 );
3061 }
3062
3063 #[test]
3064 fn test_select_outbound_gift_wrap_kind_persistent_mode_overrides_ephemeral() {
3065 assert_eq!(
3066 NostrServerTransport::select_outbound_gift_wrap_kind(
3067 GiftWrapMode::Persistent,
3068 true,
3069 Some(EPHEMERAL_GIFT_WRAP_KIND),
3070 ),
3071 Some(GIFT_WRAP_KIND)
3072 );
3073 }
3074
3075 #[test]
3076 fn test_append_common_response_tags_includes_encryption_when_optional() {
3077 let snapshot = announcement_manager::CommonTagsSnapshot {
3078 server_info: None,
3079 extra_common_tags: vec![],
3080 internal_common_tags: vec![],
3081 encryption_mode: EncryptionMode::Optional,
3082 gift_wrap_mode: GiftWrapMode::Optional,
3083 };
3084 let mut tags = Vec::new();
3085 snapshot.append_common_response_tags(&mut tags);
3086 let kinds: Vec<String> = tags.iter().map(|t| format!("{:?}", t.kind())).collect();
3087 assert!(
3088 kinds.iter().any(|k| k.contains("support_encryption")),
3089 "should include support_encryption tag"
3090 );
3091 }
3092
3093 #[test]
3094 fn test_append_common_response_tags_no_encryption_when_disabled() {
3095 let snapshot = announcement_manager::CommonTagsSnapshot {
3096 server_info: None,
3097 extra_common_tags: vec![],
3098 internal_common_tags: vec![],
3099 encryption_mode: EncryptionMode::Disabled,
3100 gift_wrap_mode: GiftWrapMode::Optional,
3101 };
3102 let mut tags = Vec::new();
3103 snapshot.append_common_response_tags(&mut tags);
3104 assert!(
3105 tags.is_empty(),
3106 "should not include encryption tags when encryption disabled"
3107 );
3108 }
3109
3110 #[test]
3111 fn test_select_outbound_notification_gift_wrap_kind_plaintext() {
3112 assert_eq!(
3113 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3114 GiftWrapMode::Optional,
3115 false,
3116 Some(EPHEMERAL_GIFT_WRAP_KIND),
3117 true,
3118 ),
3119 None
3120 );
3121 }
3122
3123 #[test]
3124 fn test_select_outbound_notification_gift_wrap_kind_mirrors_correlated() {
3125 assert_eq!(
3126 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3127 GiftWrapMode::Optional,
3128 true,
3129 Some(EPHEMERAL_GIFT_WRAP_KIND),
3130 false,
3131 ),
3132 Some(EPHEMERAL_GIFT_WRAP_KIND)
3133 );
3134 }
3135
3136 #[test]
3137 fn test_select_outbound_notification_gift_wrap_kind_falls_back_to_mode_if_correlated_not_allowed(
3138 ) {
3139 assert_eq!(
3140 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3141 GiftWrapMode::Ephemeral,
3142 true,
3143 Some(GIFT_WRAP_KIND),
3144 false,
3145 ),
3146 Some(EPHEMERAL_GIFT_WRAP_KIND)
3147 );
3148 }
3149
3150 #[test]
3151 fn test_select_outbound_notification_gift_wrap_kind_uses_ephemeral_if_supported() {
3152 assert_eq!(
3153 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3154 GiftWrapMode::Optional,
3155 true,
3156 None,
3157 true,
3158 ),
3159 Some(EPHEMERAL_GIFT_WRAP_KIND)
3160 );
3161 }
3162
3163 #[test]
3164 fn test_select_outbound_notification_gift_wrap_kind_uses_persistent_if_ephemeral_supported_but_mode_persistent(
3165 ) {
3166 assert_eq!(
3167 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3168 GiftWrapMode::Persistent,
3169 true,
3170 None,
3171 true,
3172 ),
3173 Some(GIFT_WRAP_KIND)
3174 );
3175 }
3176
3177 #[test]
3178 fn test_select_outbound_notification_gift_wrap_kind_uses_default_mode_if_ephemeral_not_supported(
3179 ) {
3180 assert_eq!(
3181 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3182 GiftWrapMode::Optional,
3183 true,
3184 None,
3185 false,
3186 ),
3187 Some(GIFT_WRAP_KIND)
3188 );
3189 }
3190
3191 #[test]
3192 fn test_append_common_response_tags_includes_ephemeral_tag() {
3193 let snapshot = announcement_manager::CommonTagsSnapshot {
3194 server_info: None,
3195 extra_common_tags: vec![],
3196 internal_common_tags: vec![],
3197 encryption_mode: EncryptionMode::Optional,
3198 gift_wrap_mode: GiftWrapMode::Optional,
3199 };
3200 let mut tags = Vec::new();
3201 snapshot.append_common_response_tags(&mut tags);
3202 let kinds: Vec<String> = tags.iter().map(|t| format!("{:?}", t.kind())).collect();
3203 assert!(
3204 kinds
3205 .iter()
3206 .any(|k| k.contains("support_encryption_ephemeral")),
3207 "should include support_encryption_ephemeral tag"
3208 );
3209 }
3210
3211 #[test]
3212 fn test_append_common_response_tags_includes_server_info() {
3213 let server_info = ServerInfo {
3214 name: Some("TestServer".to_string()),
3215 ..Default::default()
3216 };
3217 let snapshot = announcement_manager::CommonTagsSnapshot {
3218 server_info: Some(server_info),
3219 extra_common_tags: vec![],
3220 internal_common_tags: vec![],
3221 encryption_mode: EncryptionMode::Disabled,
3222 gift_wrap_mode: GiftWrapMode::Optional,
3223 };
3224 let mut tags = Vec::new();
3225 snapshot.append_common_response_tags(&mut tags);
3226 let tag_value = tags
3227 .iter()
3228 .find(|t| (*t).clone().to_vec().first().map(|s| s.as_str()) == Some("name"))
3229 .and_then(|t| t.clone().to_vec().get(1).cloned());
3230 assert_eq!(tag_value.as_deref(), Some("TestServer"));
3231 }
3232
3233 #[test]
3234 fn test_append_common_response_tags_extra_tags() {
3235 let extra_tags = vec![Tag::custom(
3236 TagKind::Custom("custom_tag".into()),
3237 vec!["value".to_string()],
3238 )];
3239 let snapshot = announcement_manager::CommonTagsSnapshot {
3240 server_info: None,
3241 extra_common_tags: extra_tags,
3242 internal_common_tags: vec![],
3243 encryption_mode: EncryptionMode::Disabled,
3244 gift_wrap_mode: GiftWrapMode::Optional,
3245 };
3246 let mut tags = Vec::new();
3247 snapshot.append_common_response_tags(&mut tags);
3248 let tag_value = tags
3249 .iter()
3250 .find(|t| (*t).clone().to_vec().first().map(|s| s.as_str()) == Some("custom_tag"))
3251 .and_then(|t| t.clone().to_vec().get(1).cloned());
3252 assert_eq!(tag_value.as_deref(), Some("value"));
3253 }
3254
3255 #[test]
3258 fn test_cep35_client_session_new_fields_default_false() {
3259 let session = ClientSession::new(false);
3260 assert!(!session.has_sent_common_tags);
3261 assert!(!session.supports_encryption);
3262 assert!(!session.supports_ephemeral_encryption);
3263 assert!(!session.supports_oversized_transfer);
3264 }
3265
3266 #[test]
3267 fn test_cep35_capability_or_assign() {
3268 let mut session = ClientSession::new(false);
3269
3270 session.supports_encryption |= true;
3271 session.supports_ephemeral_encryption |= false;
3272
3273 session.supports_encryption |= false;
3274 session.supports_ephemeral_encryption |= true;
3275
3276 assert!(session.supports_encryption, "OR-assign must not downgrade");
3277 assert!(session.supports_ephemeral_encryption);
3278 assert!(!session.supports_oversized_transfer);
3279 }
3280
3281 #[test]
3282 fn test_config_gift_wrap_mode_default() {
3283 let config = NostrServerTransportConfig::default();
3284 assert_eq!(config.gift_wrap_mode, GiftWrapMode::Optional);
3285 }
3286
3287 fn first_tag_values(tags: &[Tag]) -> Vec<String> {
3290 tags.iter().map(|t| t.clone().to_vec()[0].clone()).collect()
3291 }
3292
3293 async fn make_server_with_oversized(enabled: bool) -> NostrServerTransport {
3294 let config = NostrServerTransportConfig {
3295 oversized_transfer: OversizedTransferConfig::default().with_enabled(enabled),
3296 ..Default::default()
3297 };
3298 let pool: Arc<dyn RelayPoolTrait> = Arc::new(crate::relay::mock::MockRelayPool::new());
3299 NostrServerTransport::with_relay_pool(config, pool)
3300 .await
3301 .expect("server transport construction")
3302 }
3303
3304 #[test]
3305 fn test_oversized_enabled_by_default() {
3306 let config = NostrServerTransportConfig::default();
3307 assert!(config.oversized_transfer.enabled);
3308 }
3309
3310 #[test]
3311 fn test_oversized_support_tags_helper() {
3312 let mut config = NostrServerTransportConfig::default().with_oversized_enabled(false);
3314 assert!(oversized_support_tags(&config).is_empty());
3315 config.oversized_transfer.enabled = true;
3316 let names = first_tag_values(&oversized_support_tags(&config));
3317 assert_eq!(names, vec!["support_oversized_transfer"]);
3318 }
3319
3320 #[test]
3321 fn test_oversized_builders() {
3322 let config = NostrServerTransportConfig::default().with_oversized_enabled(true);
3323 assert!(config.oversized_transfer.enabled);
3324 let config = NostrServerTransportConfig::default()
3325 .with_oversized_transfer(OversizedTransferConfig::enabled().with_threshold(123));
3326 assert!(config.oversized_transfer.enabled);
3327 assert_eq!(config.oversized_transfer.threshold, 123);
3328 }
3329
3330 #[tokio::test]
3331 async fn test_announcement_includes_oversized_tag_when_enabled() {
3332 let server = make_server_with_oversized(true).await;
3333 let names = first_tag_values(&server.announcement_manager.get_common_tags());
3334 assert!(
3335 names.contains(&"support_oversized_transfer".to_string()),
3336 "announcement common tags must advertise oversized support when enabled"
3337 );
3338 }
3339
3340 #[tokio::test]
3341 async fn test_announcement_omits_oversized_tag_when_disabled() {
3342 let server = make_server_with_oversized(false).await;
3343 let names = first_tag_values(&server.announcement_manager.get_common_tags());
3344 assert!(
3345 !names.contains(&"support_oversized_transfer".to_string()),
3346 "announcement must not advertise oversized support when disabled"
3347 );
3348 }
3349
3350 #[tokio::test]
3351 async fn test_first_response_snapshot_includes_oversized_tag_when_enabled() {
3352 let server = make_server_with_oversized(true).await;
3353 let snapshot = server.announcement_manager.common_tags_snapshot();
3354 let mut tags = Vec::new();
3355 snapshot.append_common_response_tags(&mut tags);
3356 let names = first_tag_values(&tags);
3357 assert!(
3358 names.contains(&"support_oversized_transfer".to_string()),
3359 "first-response replay must carry the oversized tag when enabled"
3360 );
3361 }
3362
3363 #[tokio::test]
3364 async fn test_first_response_snapshot_omits_oversized_tag_when_disabled() {
3365 let server = make_server_with_oversized(false).await;
3366 let snapshot = server.announcement_manager.common_tags_snapshot();
3367 let mut tags = Vec::new();
3368 snapshot.append_common_response_tags(&mut tags);
3369 let names = first_tag_values(&tags);
3370 assert!(!names.contains(&"support_oversized_transfer".to_string()));
3371 }
3372
3373 #[test]
3374 fn test_server_learns_client_oversized_only_when_enabled() {
3375 let oversized_tag = Tag::custom(
3381 TagKind::Custom(tags::SUPPORT_OVERSIZED_TRANSFER.into()),
3382 Vec::<String>::new(),
3383 );
3384 let discovered = learn_peer_capabilities(&[oversized_tag]);
3385 assert!(discovered.supports_oversized_transfer);
3386
3387 let mut session = ClientSession::new(false);
3389 let oversized_enabled = false;
3390 session.supports_oversized_transfer |=
3391 oversized_enabled && discovered.supports_oversized_transfer;
3392 assert!(!session.supports_oversized_transfer);
3393
3394 let oversized_enabled = true;
3396 session.supports_oversized_transfer |=
3397 oversized_enabled && discovered.supports_oversized_transfer;
3398 assert!(session.supports_oversized_transfer);
3399 }
3400
3401 #[test]
3404 fn test_open_stream_support_tags_helper() {
3405 assert!(open_stream_support_tags(&OpenStreamConfig::default()).is_empty());
3407 let names = first_tag_values(&open_stream_support_tags(&OpenStreamConfig::enabled()));
3408 assert_eq!(names, vec!["support_open_stream"]);
3409 }
3410
3411 #[test]
3412 fn test_internal_common_capability_tags_merges_both() {
3413 let config = NostrServerTransportConfig::default()
3414 .with_oversized_enabled(true)
3415 .with_open_stream(OpenStreamConfig::enabled());
3416 let names = first_tag_values(&internal_common_capability_tags(&config));
3417 assert!(names.contains(&"support_oversized_transfer".to_string()));
3418 assert!(names.contains(&"support_open_stream".to_string()));
3419 }
3420
3421 #[tokio::test]
3422 async fn test_announcement_includes_open_stream_tag_when_enabled() {
3423 let config = NostrServerTransportConfig {
3424 open_stream: OpenStreamConfig::enabled(),
3425 ..Default::default()
3426 };
3427 let pool: Arc<dyn RelayPoolTrait> = Arc::new(crate::relay::mock::MockRelayPool::new());
3428 let server = NostrServerTransport::with_relay_pool(config, pool)
3429 .await
3430 .expect("server transport construction");
3431 let names = first_tag_values(&server.announcement_manager.get_common_tags());
3432 assert!(
3433 names.contains(&"support_open_stream".to_string()),
3434 "announcement must advertise open-stream support when enabled"
3435 );
3436 }
3437
3438 #[tokio::test]
3439 async fn test_announcement_omits_open_stream_tag_when_disabled() {
3440 let pool: Arc<dyn RelayPoolTrait> = Arc::new(crate::relay::mock::MockRelayPool::new());
3442 let server =
3443 NostrServerTransport::with_relay_pool(NostrServerTransportConfig::default(), pool)
3444 .await
3445 .expect("server transport construction");
3446 let names = first_tag_values(&server.announcement_manager.get_common_tags());
3447 assert!(!names.contains(&"support_open_stream".to_string()));
3448 }
3449
3450 #[test]
3451 fn test_server_learns_client_open_stream_only_when_enabled() {
3452 let open_stream_tag = Tag::custom(
3453 TagKind::Custom(tags::SUPPORT_OPEN_STREAM.into()),
3454 Vec::<String>::new(),
3455 );
3456 let discovered = learn_peer_capabilities(&[open_stream_tag]);
3457 assert!(discovered.supports_open_stream);
3458
3459 let mut session = ClientSession::new(false);
3461 let open_stream_enabled = false;
3462 session.supports_open_stream |= open_stream_enabled && discovered.supports_open_stream;
3463 assert!(!session.supports_open_stream);
3464
3465 let open_stream_enabled = true;
3467 session.supports_open_stream |= open_stream_enabled && discovered.supports_open_stream;
3468 assert!(session.supports_open_stream);
3469 }
3470
3471 fn deferral_test_writer(token: &str) -> OpenStreamWriter {
3475 let publish_frame: PublishFrame = Arc::new(|_frame: JsonRpcNotification| {
3476 Box::pin(async move { Ok(EventId::all_zeros()) })
3477 });
3478 OpenStreamWriter::new(OpenStreamWriterOptions {
3479 progress_token: token.to_string(),
3480 publish_frame,
3481 content_type: None,
3482 on_close: None,
3483 on_abort: None,
3484 })
3485 }
3486
3487 fn install_slot(
3490 state: &ServerOpenStreamState,
3491 event_id: &str,
3492 writer: OpenStreamWriter,
3493 terminated: bool,
3494 ) {
3495 let token = writer.progress_token().to_string();
3496 let snapshot = RouteSnapshot {
3497 client_pubkey: Keys::generate().public_key(),
3498 original_request_id: serde_json::json!(1),
3499 is_encrypted: false,
3500 mirrored_wrap_kind: None,
3501 };
3502 state.lock_slots().insert(
3503 event_id.to_string(),
3504 OpenStreamSlot {
3505 writer,
3506 snapshot,
3507 pending_response: None,
3508 terminated,
3509 },
3510 );
3511 state.lock_token_index().insert(token, event_id.to_string());
3512 }
3513
3514 fn dummy_response() -> JsonRpcMessage {
3515 JsonRpcMessage::Response(JsonRpcResponse {
3516 jsonrpc: "2.0".to_string(),
3517 id: serde_json::json!(1),
3518 result: serde_json::json!({ "ok": true }),
3519 })
3520 }
3521
3522 #[tokio::test]
3523 async fn try_defer_open_stream_response_branch_coverage() {
3524 let config = NostrServerTransportConfig::default()
3525 .with_open_stream(OpenStreamConfig::default().with_enabled(true));
3526 let pool: Arc<dyn RelayPoolTrait> = Arc::new(MockRelayPool::new());
3527 let transport = NostrServerTransport::with_relay_pool(config, pool)
3528 .await
3529 .expect("server transport");
3530
3531 assert!(matches!(
3533 transport.try_defer_open_stream_response("evt-none", dummy_response()),
3534 OpenStreamDeferral::Passthrough(_)
3535 ));
3536
3537 install_slot(
3541 &transport.open_stream,
3542 "evt-unstarted",
3543 deferral_test_writer("tok-unstarted"),
3544 false,
3545 );
3546 assert!(matches!(
3547 transport.try_defer_open_stream_response("evt-unstarted", dummy_response()),
3548 OpenStreamDeferral::Passthrough(_)
3549 ));
3550 assert!(
3551 transport
3552 .open_stream
3553 .lock_slots()
3554 .get("evt-unstarted")
3555 .is_none(),
3556 "unstarted writer slot must be removed (no leak)"
3557 );
3558 assert!(
3559 transport
3560 .open_stream
3561 .lock_token_index()
3562 .get("tok-unstarted")
3563 .is_none(),
3564 "unstarted writer token index must be removed (no leak)"
3565 );
3566
3567 let terminal = deferral_test_writer("tok-terminal");
3571 terminal.start().await.expect("start");
3572 install_slot(&transport.open_stream, "evt-terminal", terminal, true);
3573 assert!(matches!(
3574 transport.try_defer_open_stream_response("evt-terminal", dummy_response()),
3575 OpenStreamDeferral::SendNow { .. }
3576 ));
3577 assert!(transport
3578 .open_stream
3579 .lock_slots()
3580 .get("evt-terminal")
3581 .is_none());
3582 assert!(transport
3583 .open_stream
3584 .lock_token_index()
3585 .get("tok-terminal")
3586 .is_none());
3587
3588 let open = deferral_test_writer("tok-open");
3592 open.start().await.expect("start");
3593 install_slot(&transport.open_stream, "evt-open", open, false);
3594 assert!(matches!(
3595 transport.try_defer_open_stream_response("evt-open", dummy_response()),
3596 OpenStreamDeferral::Deferred
3597 ));
3598 {
3599 let slots = transport.open_stream.lock_slots();
3600 let slot = slots.get("evt-open").expect("deferred slot retained");
3601 assert!(
3602 slot.pending_response.is_some(),
3603 "the deferred response must be stashed for the hook to flush"
3604 );
3605 }
3606
3607 let disabled = NostrServerTransport::with_relay_pool(
3610 NostrServerTransportConfig::default()
3611 .with_open_stream(OpenStreamConfig::default().with_enabled(false)),
3612 Arc::new(MockRelayPool::new()) as Arc<dyn RelayPoolTrait>,
3613 )
3614 .await
3615 .expect("disabled server transport");
3616 install_slot(
3617 &disabled.open_stream,
3618 "evt-disabled",
3619 deferral_test_writer("tok-disabled"),
3620 false,
3621 );
3622 assert!(
3623 disabled.get_open_stream_writer("evt-disabled").is_none(),
3624 "a disabled server must not expose writers (deferral never attempted)"
3625 );
3626 }
3627}