1pub mod correlation_store;
7pub mod relay_resolution;
8pub mod server_identity;
9pub mod server_relay_discovery;
10
11pub use correlation_store::ClientCorrelationStore;
12
13use std::collections::{HashMap, VecDeque};
14use std::num::NonZeroUsize;
15use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
16use std::sync::{Arc, Mutex};
17use std::time::{Duration, Instant};
18
19use lru::LruCache;
20use nostr_sdk::prelude::*;
21use tokio::sync::oneshot;
22use tokio::sync::Mutex as AsyncMutex;
23use tokio_util::sync::CancellationToken;
24
25use crate::core::constants::*;
26use crate::core::error::Result;
27use crate::core::serializers;
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::{parse_discovered_peer_capabilities, PeerCapabilities};
34use crate::transport::open_stream::{
35 FrameOutcome, KeepaliveAction, OpenStreamConfig, OpenStreamFrame, OpenStreamReceiver,
36 OpenStreamRegistry, OpenStreamSession, OpenStreamSessionInit, PublishFrame,
37};
38use crate::transport::oversized_transfer::{
39 build_oversized_frames, progress_token_string, resolve_safe_chunk_size,
40 send_oversized_transfer, OversizedFrame, OversizedSenderOptions, OversizedTransferConfig,
41 OversizedTransferReceiver, NOTIFICATIONS_PROGRESS_METHOD,
42};
43
44const LOG_TARGET: &str = "contextvm_sdk::transport::client";
45
46#[derive(Debug, Clone)]
48#[non_exhaustive]
49pub struct NostrClientTransportConfig {
50 pub relay_urls: Vec<String>,
52 pub server_pubkey: String,
57 pub encryption_mode: EncryptionMode,
59 pub gift_wrap_mode: GiftWrapMode,
61 pub is_stateless: bool,
63 pub timeout: Duration,
69 pub discovery_relay_urls: Option<Vec<String>>,
72 pub fallback_operational_relay_urls: Option<Vec<String>>,
74 pub oversized_transfer: OversizedTransferConfig,
76 pub open_stream: OpenStreamConfig,
82}
83
84impl Default for NostrClientTransportConfig {
85 fn default() -> Self {
86 Self {
87 relay_urls: vec![],
88 server_pubkey: String::new(),
89 encryption_mode: EncryptionMode::Optional,
90 gift_wrap_mode: GiftWrapMode::Optional,
91 is_stateless: false,
92 timeout: Duration::from_secs(30),
93 discovery_relay_urls: None,
94 fallback_operational_relay_urls: None,
95 oversized_transfer: OversizedTransferConfig::default(),
96 open_stream: OpenStreamConfig::default(),
97 }
98 }
99}
100
101impl NostrClientTransportConfig {
102 pub fn with_server_pubkey(mut self, pubkey: impl Into<String>) -> Self {
104 self.server_pubkey = pubkey.into();
105 self
106 }
107 pub fn with_encryption_mode(mut self, mode: EncryptionMode) -> Self {
109 self.encryption_mode = mode;
110 self
111 }
112 pub fn with_gift_wrap_mode(mut self, mode: GiftWrapMode) -> Self {
114 self.gift_wrap_mode = mode;
115 self
116 }
117 pub fn with_stateless(mut self, stateless: bool) -> Self {
119 self.is_stateless = stateless;
120 self
121 }
122 pub fn with_relay_urls(mut self, urls: Vec<String>) -> Self {
124 self.relay_urls = urls;
125 self
126 }
127 pub fn with_timeout(mut self, timeout: Duration) -> Self {
129 self.timeout = timeout;
130 self
131 }
132 pub fn with_discovery_relay_urls(mut self, urls: Vec<String>) -> Self {
134 self.discovery_relay_urls = Some(urls);
135 self
136 }
137 pub fn with_fallback_operational_relay_urls(mut self, urls: Vec<String>) -> Self {
139 self.fallback_operational_relay_urls = Some(urls);
140 self
141 }
142 pub fn with_oversized_transfer(mut self, config: OversizedTransferConfig) -> Self {
144 self.oversized_transfer = config;
145 self
146 }
147 pub fn with_oversized_enabled(mut self, enabled: bool) -> Self {
149 self.oversized_transfer.enabled = enabled;
150 self
151 }
152 pub fn with_open_stream(mut self, config: OpenStreamConfig) -> Self {
155 self.open_stream = config;
156 self
157 }
158}
159
160pub struct NostrClientTransport {
162 base: BaseTransport,
163 config: NostrClientTransportConfig,
164 server_pubkey: PublicKey,
165 hinted_relay_urls: Vec<String>,
167 discovery_relay_urls: Vec<String>,
169 fallback_operational_relay_urls: Vec<String>,
171 pending_requests: ClientCorrelationStore,
173 has_sent_discovery_tags: AtomicBool,
175 discovered_server_capabilities: Arc<Mutex<PeerCapabilities>>,
177 server_initialize_event: Arc<Mutex<Option<Event>>>,
179 server_supports_ephemeral: Arc<AtomicBool>,
181 seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
185 oversized_receiver: Arc<Mutex<OversizedTransferReceiver>>,
188 accept_waiters: Arc<Mutex<HashMap<String, oneshot::Sender<()>>>>,
192 original_progress_tokens: Arc<Mutex<LruCache<String, serde_json::Value>>>,
200 open_stream_registry: Arc<AsyncMutex<OpenStreamRegistry>>,
204 #[allow(clippy::type_complexity)]
208 pending_outbound_open_stream:
209 Arc<Mutex<VecDeque<oneshot::Sender<Result<(String, OpenStreamSession)>>>>>,
210 open_stream_control_progress: Arc<AtomicU64>,
214 open_stream_bind_lock: Arc<AsyncMutex<()>>,
220 message_tx: Option<tokio::sync::mpsc::UnboundedSender<JsonRpcMessage>>,
222 message_rx: Option<tokio::sync::mpsc::UnboundedReceiver<JsonRpcMessage>>,
223 cancellation_token: CancellationToken,
225 event_loop_handle: Option<tokio::task::JoinHandle<()>>,
227}
228
229#[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
240#[derive(Clone)]
241pub struct ClientOpenStreamHandle {
242 registry: Arc<AsyncMutex<OpenStreamRegistry>>,
243 #[allow(clippy::type_complexity)]
244 pending: Arc<Mutex<VecDeque<oneshot::Sender<Result<(String, OpenStreamSession)>>>>>,
245 bind_lock: Arc<AsyncMutex<()>>,
246 config: OpenStreamConfig,
247}
248
249#[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
250impl ClientOpenStreamHandle {
251 pub(crate) fn prepare_outbound(
254 &self,
255 ) -> oneshot::Receiver<Result<(String, OpenStreamSession)>> {
256 let (tx, rx) = oneshot::channel();
257 let mut pending = match self.pending.lock() {
258 Ok(g) => g,
259 Err(p) => p.into_inner(),
260 };
261 pending.push_back(tx);
262 rx
263 }
264
265 pub(crate) fn cancel_outbound(&self) {
272 let mut pending = match self.pending.lock() {
273 Ok(g) => g,
274 Err(p) => p.into_inner(),
275 };
276 pending.pop_back();
277 }
278
279 pub(crate) fn registry(&self) -> Arc<AsyncMutex<OpenStreamRegistry>> {
281 self.registry.clone()
282 }
283
284 pub(crate) fn bind_lock(&self) -> &Arc<AsyncMutex<()>> {
286 &self.bind_lock
287 }
288
289 pub(crate) fn config(&self) -> &OpenStreamConfig {
291 &self.config
292 }
293}
294
295impl std::fmt::Debug for ClientOpenStreamHandle {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 f.debug_struct("ClientOpenStreamHandle")
298 .finish_non_exhaustive()
299 }
300}
301
302impl NostrClientTransport {
303 pub async fn new<T>(signer: T, config: NostrClientTransportConfig) -> Result<Self>
305 where
306 T: IntoNostrSigner,
307 {
308 let (server_pubkey, hinted_relay_urls) =
309 server_identity::parse_server_identity(&config.server_pubkey).map_err(|error| {
310 tracing::error!(
311 target: LOG_TARGET,
312 error = %error,
313 server_pubkey = %config.server_pubkey,
314 "Invalid server pubkey"
315 );
316 error
317 })?;
318
319 let relay_pool: Arc<dyn RelayPoolTrait> =
320 Arc::new(RelayPool::new(signer).await.map_err(|error| {
321 tracing::error!(
322 target: LOG_TARGET,
323 error = %error,
324 "Failed to initialize relay pool for client transport"
325 );
326 error
327 })?);
328 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
329 let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new(
330 NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
331 )));
332
333 tracing::info!(
334 target: LOG_TARGET,
335 relay_count = config.relay_urls.len(),
336 stateless = config.is_stateless,
337 encryption_mode = ?config.encryption_mode,
338 "Created client transport"
339 );
340 let discovery_relay_urls = config.discovery_relay_urls.clone().unwrap_or_else(|| {
341 DEFAULT_BOOTSTRAP_RELAY_URLS
342 .iter()
343 .map(|s| s.to_string())
344 .collect()
345 });
346 let fallback_operational_relay_urls = config
347 .fallback_operational_relay_urls
348 .clone()
349 .unwrap_or_default();
350
351 let oversized_receiver = Arc::new(Mutex::new(OversizedTransferReceiver::with_policy(
352 (&config.oversized_transfer).into(),
353 )));
354 let accept_waiters = Arc::new(Mutex::new(HashMap::new()));
355 let original_progress_tokens = Arc::new(Mutex::new(LruCache::new(
356 NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
357 )));
358 let open_stream_registry = Arc::new(AsyncMutex::new(OpenStreamRegistry::with_policy(
359 (&config.open_stream).into(),
360 )));
361
362 Ok(Self {
363 base: BaseTransport {
364 relay_pool,
365 encryption_mode: config.encryption_mode,
366 is_connected: false,
367 },
368 oversized_receiver,
369 accept_waiters,
370 original_progress_tokens,
371 open_stream_registry,
372 pending_outbound_open_stream: Arc::new(Mutex::new(VecDeque::new())),
373 open_stream_control_progress: Arc::new(AtomicU64::new(0)),
374 open_stream_bind_lock: Arc::new(AsyncMutex::new(())),
375 config,
376 server_pubkey,
377 hinted_relay_urls,
378 discovery_relay_urls,
379 fallback_operational_relay_urls,
380 pending_requests: ClientCorrelationStore::new(),
381 has_sent_discovery_tags: AtomicBool::new(false),
382 discovered_server_capabilities: Arc::new(Mutex::new(PeerCapabilities::default())),
383 server_initialize_event: Arc::new(Mutex::new(None)),
384 server_supports_ephemeral: Arc::new(AtomicBool::new(false)),
385 seen_gift_wrap_ids,
386 message_tx: Some(tx),
387 message_rx: Some(rx),
388 cancellation_token: CancellationToken::new(),
389 event_loop_handle: None,
390 })
391 }
392
393 pub async fn with_relay_pool(
395 config: NostrClientTransportConfig,
396 relay_pool: Arc<dyn RelayPoolTrait>,
397 ) -> Result<Self> {
398 let (server_pubkey, hinted_relay_urls) =
399 server_identity::parse_server_identity(&config.server_pubkey).map_err(|error| {
400 tracing::error!(
401 target: LOG_TARGET,
402 error = %error,
403 server_pubkey = %config.server_pubkey,
404 "Invalid server pubkey"
405 );
406 error
407 })?;
408
409 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
410 let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new(
411 NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
412 )));
413
414 let discovery_relay_urls = config.discovery_relay_urls.clone().unwrap_or_else(|| {
415 DEFAULT_BOOTSTRAP_RELAY_URLS
416 .iter()
417 .map(|s| s.to_string())
418 .collect()
419 });
420 let fallback_operational_relay_urls = config
421 .fallback_operational_relay_urls
422 .clone()
423 .unwrap_or_default();
424
425 tracing::info!(
426 target: LOG_TARGET,
427 relay_count = config.relay_urls.len(),
428 stateless = config.is_stateless,
429 encryption_mode = ?config.encryption_mode,
430 "Created client transport (with_relay_pool)"
431 );
432 let oversized_receiver = Arc::new(Mutex::new(OversizedTransferReceiver::with_policy(
433 (&config.oversized_transfer).into(),
434 )));
435 let accept_waiters = Arc::new(Mutex::new(HashMap::new()));
436 let original_progress_tokens = Arc::new(Mutex::new(LruCache::new(
437 NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
438 )));
439 let open_stream_registry = Arc::new(AsyncMutex::new(OpenStreamRegistry::with_policy(
440 (&config.open_stream).into(),
441 )));
442
443 Ok(Self {
444 base: BaseTransport {
445 relay_pool,
446 encryption_mode: config.encryption_mode,
447 is_connected: false,
448 },
449 oversized_receiver,
450 accept_waiters,
451 original_progress_tokens,
452 open_stream_registry,
453 pending_outbound_open_stream: Arc::new(Mutex::new(VecDeque::new())),
454 open_stream_control_progress: Arc::new(AtomicU64::new(0)),
455 open_stream_bind_lock: Arc::new(AsyncMutex::new(())),
456 config,
457 server_pubkey,
458 hinted_relay_urls,
459 discovery_relay_urls,
460 fallback_operational_relay_urls,
461 pending_requests: ClientCorrelationStore::new(),
462 has_sent_discovery_tags: AtomicBool::new(false),
463 discovered_server_capabilities: Arc::new(Mutex::new(PeerCapabilities::default())),
464 server_initialize_event: Arc::new(Mutex::new(None)),
465 server_supports_ephemeral: Arc::new(AtomicBool::new(false)),
466 seen_gift_wrap_ids,
467 message_tx: Some(tx),
468 message_rx: Some(rx),
469 cancellation_token: CancellationToken::new(),
470 event_loop_handle: None,
471 })
472 }
473
474 pub async fn start(&mut self) -> Result<()> {
476 let resolved_urls =
477 relay_resolution::resolve_operational_relays(relay_resolution::RelayResolutionConfig {
478 configured_relay_urls: self.config.relay_urls.clone(),
479 hinted_relay_urls: self.hinted_relay_urls.clone(),
480 discovery_relay_urls: self.discovery_relay_urls.clone(),
481 fallback_operational_relay_urls: self.fallback_operational_relay_urls.clone(),
482 server_pubkey: self.server_pubkey,
483 signer: self.base.relay_pool.signer().await?,
484 timeout: Duration::from_millis(DEFAULT_TIMEOUT_MS),
485 })
486 .await;
487
488 let connect_urls = if resolved_urls.is_empty() {
489 &self.config.relay_urls
490 } else {
491 &resolved_urls
492 };
493
494 self.base.connect(connect_urls).await.map_err(|error| {
495 tracing::error!(
496 target: LOG_TARGET,
497 error = %error,
498 "Failed to connect client transport to relays"
499 );
500 error
501 })?;
502
503 let pubkey = self.base.get_public_key().await.map_err(|error| {
504 tracing::error!(
505 target: LOG_TARGET,
506 error = %error,
507 "Failed to fetch client transport public key"
508 );
509 error
510 })?;
511 tracing::info!(
512 target: LOG_TARGET,
513 pubkey = %pubkey.to_hex(),
514 "Client transport started"
515 );
516
517 self.base
518 .subscribe_for_pubkey(&pubkey)
519 .await
520 .map_err(|error| {
521 tracing::error!(
522 target: LOG_TARGET,
523 error = %error,
524 pubkey = %pubkey.to_hex(),
525 "Failed to subscribe client transport for pubkey"
526 );
527 error
528 })?;
529
530 let relay_pool = Arc::clone(&self.base.relay_pool);
532 let pending = self.pending_requests.clone();
533 let server_pubkey = self.server_pubkey;
534 let tx = self
535 .message_tx
536 .as_ref()
537 .expect("message_tx must exist before start()")
538 .clone();
539 let encryption_mode = self.config.encryption_mode;
540 let gift_wrap_mode = self.config.gift_wrap_mode;
541 let discovered_caps = self.discovered_server_capabilities.clone();
542 let init_event = self.server_initialize_event.clone();
543 let server_supports_ephemeral = self.server_supports_ephemeral.clone();
544 let seen_gift_wrap_ids = self.seen_gift_wrap_ids.clone();
545 let oversized_receiver = self.oversized_receiver.clone();
546 let accept_waiters = self.accept_waiters.clone();
547 let original_progress_tokens = self.original_progress_tokens.clone();
548 let oversized_enabled = self.config.oversized_transfer.enabled;
549 let open_stream_registry = self.open_stream_registry.clone();
550 let open_stream_control_progress = self.open_stream_control_progress.clone();
551 let open_stream_enabled = self.config.open_stream.enabled;
552 let timeout = self.config.timeout;
553 let token = self.cancellation_token.child_token();
554
555 self.event_loop_handle = Some(tokio::spawn(async move {
556 Self::event_loop(
557 relay_pool,
558 pending,
559 server_pubkey,
560 tx,
561 encryption_mode,
562 gift_wrap_mode,
563 discovered_caps,
564 init_event,
565 server_supports_ephemeral,
566 seen_gift_wrap_ids,
567 oversized_receiver,
568 accept_waiters,
569 original_progress_tokens,
570 oversized_enabled,
571 open_stream_registry,
572 open_stream_control_progress,
573 open_stream_enabled,
574 timeout,
575 token,
576 )
577 .await;
578 }));
579
580 tracing::info!(
581 target: LOG_TARGET,
582 relay_count = self.config.relay_urls.len(),
583 "Client transport event loop spawned"
584 );
585 Ok(())
586 }
587
588 pub async fn close(&mut self) -> Result<()> {
590 self.cancellation_token.cancel();
591 if let Some(handle) = self.event_loop_handle.take() {
592 let _ = handle.await;
593 }
594 self.message_tx.take();
595 {
599 let mut receiver = match self.oversized_receiver.lock() {
600 Ok(g) => g,
601 Err(p) => p.into_inner(),
602 };
603 receiver.clear();
604 }
605 {
606 let mut waiters = match self.accept_waiters.lock() {
607 Ok(g) => g,
608 Err(p) => p.into_inner(),
609 };
610 waiters.clear();
611 }
612 {
613 let mut originals = match self.original_progress_tokens.lock() {
614 Ok(g) => g,
615 Err(p) => p.into_inner(),
616 };
617 originals.clear();
618 }
619 self.open_stream_registry.lock().await.clear();
622 {
623 let mut pending = match self.pending_outbound_open_stream.lock() {
624 Ok(g) => g,
625 Err(p) => p.into_inner(),
626 };
627 pending.clear();
628 }
629 self.base.disconnect().await
630 }
631
632 pub async fn send(&self, message: &JsonRpcMessage) -> Result<()> {
634 if self.config.is_stateless {
636 if let JsonRpcMessage::Request(ref req) = message {
637 if req.method == "initialize" {
638 self.emulate_initialize_response(&req.id);
639 return Ok(());
640 }
641 }
642 if let JsonRpcMessage::Notification(ref n) = message {
643 if n.method == "notifications/initialized" {
644 return Ok(());
645 }
646 }
647 }
648
649 let is_request = message.is_request();
650
651 if is_request && self.config.open_stream.enabled {
657 if let JsonRpcMessage::Request(req) = message {
658 if req.method == "tools/call" {
659 if let Some(original) = req
660 .params
661 .as_ref()
662 .and_then(|p| p.get("_meta"))
663 .and_then(|m| m.get("progressToken"))
664 {
665 if let Some(token) = progress_token_string(original) {
666 self.bind_pending_outbound_open_stream(&token, original)
667 .await;
668 }
669 }
670 }
671 }
672 }
673
674 let base_tags = BaseTransport::create_recipient_tags(&self.server_pubkey);
675 let discovery_tags = if is_request {
676 self.get_pending_client_discovery_tags()
677 } else {
678 vec![]
679 };
680 let tags = BaseTransport::compose_outbound_tags(&base_tags, &discovery_tags, &[]);
681 let gift_wrap_kind = self.choose_outbound_gift_wrap_kind();
682 let discovery_sent = !discovery_tags.is_empty();
683
684 let oversized_token: Option<String> =
691 if is_request && self.config.oversized_transfer.enabled {
692 let original = match message {
693 JsonRpcMessage::Request(req) => req
694 .params
695 .as_ref()
696 .and_then(|p| p.get("_meta"))
697 .and_then(|m| m.get("progressToken")),
698 _ => None,
699 };
700 let token = original.and_then(progress_token_string);
701 if let (Some(token), Some(original)) = (token.as_deref(), original) {
702 self.record_original_progress_token(token, original);
703 }
704 token
705 } else {
706 None
707 };
708
709 if let Some(token) = oversized_token.as_deref() {
719 let content = serde_json::to_string(message)?;
720 let threshold = self.config.oversized_transfer.threshold;
721 if content.len() >= threshold {
722 return self
723 .send_oversized_request(
724 message,
725 &content,
726 token,
727 base_tags,
728 tags,
729 discovery_sent,
730 )
731 .await;
732 }
733 match self
737 .base
738 .prepare_mcp_message(
739 message,
740 &self.server_pubkey,
741 CTXVM_MESSAGES_KIND,
742 tags.clone(),
743 None,
744 Some(gift_wrap_kind),
745 )
746 .await
747 {
748 Ok((event_id, publishable_event)) => {
749 let published_len = serde_json::to_string(&publishable_event)
750 .map(|s| s.len())
751 .unwrap_or(usize::MAX);
752 if published_len > threshold {
753 return self
754 .send_oversized_request(
755 message,
756 &content,
757 token,
758 base_tags,
759 tags,
760 discovery_sent,
761 )
762 .await;
763 }
764 return self
765 .publish_single_event(message, event_id, publishable_event, discovery_sent)
766 .await;
767 }
768 Err(error) => {
769 tracing::debug!(
773 target: LOG_TARGET,
774 error = %error,
775 "Single-event build failed; sending as oversized transfer"
776 );
777 return self
778 .send_oversized_request(
779 message,
780 &content,
781 token,
782 base_tags,
783 tags,
784 discovery_sent,
785 )
786 .await;
787 }
788 }
789 }
790
791 let (event_id, publishable_event) = self
794 .base
795 .prepare_mcp_message(
796 message,
797 &self.server_pubkey,
798 CTXVM_MESSAGES_KIND,
799 tags,
800 None,
801 Some(gift_wrap_kind),
802 )
803 .await
804 .map_err(|error| {
805 tracing::error!(
806 target: LOG_TARGET,
807 error = %error,
808 server_pubkey = %self.server_pubkey.to_hex(),
809 method = ?message.method(),
810 "Failed to prepare client message"
811 );
812 error
813 })?;
814
815 self.publish_single_event(message, event_id, publishable_event, discovery_sent)
816 .await
817 }
818
819 async fn publish_single_event(
824 &self,
825 message: &JsonRpcMessage,
826 event_id: EventId,
827 publishable_event: Event,
828 discovery_sent: bool,
829 ) -> Result<()> {
830 if let JsonRpcMessage::Request(ref req) = message {
831 let is_initialize = req.method == INITIALIZE_METHOD;
832 self.pending_requests
833 .register(event_id.to_hex(), req.id.clone(), is_initialize)
834 .await;
835 }
836
837 if let Err(error) = self.base.relay_pool.publish_event(&publishable_event).await {
838 self.pending_requests.remove(&event_id.to_hex()).await;
839 tracing::error!(
840 target: LOG_TARGET,
841 error = %error,
842 server_pubkey = %self.server_pubkey.to_hex(),
843 method = ?message.method(),
844 "Failed to publish client message"
845 );
846 return Err(error);
847 }
848
849 if discovery_sent {
851 self.has_sent_discovery_tags.store(true, Ordering::Relaxed);
852 }
853
854 tracing::debug!(
855 target: LOG_TARGET,
856 event_id = %event_id.to_hex(),
857 method = ?message.method(),
858 "Sent client message"
859 );
860 Ok(())
861 }
862
863 async fn send_oversized_request(
871 &self,
872 message: &JsonRpcMessage,
873 content: &str,
874 token: &str,
875 base_tags: Vec<Tag>,
876 start_tags: Vec<Tag>,
877 discovery_sent: bool,
878 ) -> Result<()> {
879 let needs_accept = !self
882 .discovered_server_capabilities()
883 .supports_oversized_transfer;
884
885 let gift_wrap_kind = self.choose_outbound_gift_wrap_kind();
886 let is_encrypted = self.base.should_encrypt(CTXVM_MESSAGES_KIND, None);
890
891 let chunk_size = resolve_safe_chunk_size(
897 self.config.oversized_transfer.chunk_size,
898 &self.base,
899 &self.server_pubkey,
900 &base_tags,
901 is_encrypted,
902 Kind::Custom(gift_wrap_kind),
903 self.config.oversized_transfer.threshold,
904 )
905 .await?;
906
907 let options = OversizedSenderOptions::new(token)
908 .with_chunk_size(chunk_size)
909 .with_accept_handshake(needs_accept);
910 let frames = build_oversized_frames(content, &options)?;
911
912 let await_accept = if needs_accept {
915 let (accept_tx, accept_rx) = oneshot::channel();
916 {
917 let mut waiters = match self.accept_waiters.lock() {
918 Ok(g) => g,
919 Err(p) => p.into_inner(),
920 };
921 waiters.insert(token.to_string(), accept_tx);
922 }
923 Some(accept_rx)
924 } else {
925 None
926 };
927
928 let base = &self.base;
931 let server_pubkey = self.server_pubkey;
932 let mut start_tags = Some(start_tags);
933 let publish = move |frame: JsonRpcNotification| {
934 let tags = start_tags.take().unwrap_or_else(|| base_tags.clone());
935 async move {
936 let msg = JsonRpcMessage::Notification(frame);
937 let (event_id, publishable) = base
938 .prepare_mcp_message(
939 &msg,
940 &server_pubkey,
941 CTXVM_MESSAGES_KIND,
942 tags,
943 None,
944 Some(gift_wrap_kind),
945 )
946 .await?;
947 base.relay_pool.publish_event(&publishable).await?;
948 Ok::<EventId, crate::core::error::Error>(event_id)
949 }
950 };
951
952 let accept_timeout =
953 Duration::from_millis(self.config.oversized_transfer.accept_timeout_ms);
954 let result =
955 send_oversized_transfer(frames, needs_accept, await_accept, accept_timeout, publish)
956 .await;
957
958 if needs_accept {
960 let mut waiters = match self.accept_waiters.lock() {
961 Ok(g) => g,
962 Err(p) => p.into_inner(),
963 };
964 waiters.remove(token);
965 }
966
967 let end_id = match result {
968 Ok(id) => id,
969 Err(error) => {
970 tracing::error!(
971 target: LOG_TARGET,
972 error = %error,
973 server_pubkey = %self.server_pubkey.to_hex(),
974 method = ?message.method(),
975 "Failed to send oversized client request"
976 );
977 return Err(error);
978 }
979 };
980
981 if let JsonRpcMessage::Request(ref req) = message {
983 let is_initialize = req.method == INITIALIZE_METHOD;
984 self.pending_requests
985 .register(end_id.to_hex(), req.id.clone(), is_initialize)
986 .await;
987 }
988
989 if discovery_sent {
991 self.has_sent_discovery_tags.store(true, Ordering::Relaxed);
992 }
993
994 tracing::debug!(
995 target: LOG_TARGET,
996 end_event_id = %end_id.to_hex(),
997 method = ?message.method(),
998 "Sent oversized client request"
999 );
1000 Ok(())
1001 }
1002
1003 fn record_original_progress_token(&self, token: &str, original: &serde_json::Value) {
1007 let mut originals = match self.original_progress_tokens.lock() {
1008 Ok(g) => g,
1009 Err(p) => p.into_inner(),
1010 };
1011 originals.push(token.to_string(), original.clone());
1012 }
1013
1014 fn remove_original_progress_token(
1017 originals: &Mutex<LruCache<String, serde_json::Value>>,
1018 token: Option<&str>,
1019 ) -> Option<serde_json::Value> {
1020 let token = token?;
1021 let mut originals = match originals.lock() {
1022 Ok(g) => g,
1023 Err(p) => p.into_inner(),
1024 };
1025 originals.pop(token)
1026 }
1027
1028 fn original_progress_token(
1032 originals: &Mutex<LruCache<String, serde_json::Value>>,
1033 token: &str,
1034 ) -> Option<serde_json::Value> {
1035 let mut originals = match originals.lock() {
1036 Ok(g) => g,
1037 Err(p) => p.into_inner(),
1038 };
1039 originals.get(token).cloned()
1040 }
1041
1042 fn stripped_progress_notification(
1052 params: &serde_json::Value,
1053 original_token: &serde_json::Value,
1054 ) -> Option<JsonRpcMessage> {
1055 let mut stripped = serde_json::Map::new();
1056 stripped.insert("progressToken".to_string(), original_token.clone());
1057 stripped.insert("progress".to_string(), params.get("progress")?.clone());
1058 for key in ["total", "message"] {
1059 if let Some(value) = params.get(key) {
1060 stripped.insert(key.to_string(), value.clone());
1061 }
1062 }
1063 Some(JsonRpcMessage::Notification(JsonRpcNotification {
1064 jsonrpc: "2.0".to_string(),
1065 method: NOTIFICATIONS_PROGRESS_METHOD.to_string(),
1066 params: Some(serde_json::Value::Object(stripped)),
1067 }))
1068 }
1069
1070 fn forward_stripped_progress(
1077 notif: &JsonRpcNotification,
1078 token: &str,
1079 originals: &Mutex<LruCache<String, serde_json::Value>>,
1080 tx: &tokio::sync::mpsc::UnboundedSender<JsonRpcMessage>,
1081 ) {
1082 let Some(params) = notif.params.as_ref() else {
1083 return;
1084 };
1085 let Some(original) = Self::original_progress_token(originals, token)
1086 .or_else(|| params.get("progressToken").cloned())
1087 else {
1088 return;
1089 };
1090 if let Some(stripped) = Self::stripped_progress_notification(params, &original) {
1091 let _ = tx.send(stripped);
1092 }
1093 }
1094
1095 pub fn take_message_receiver(
1097 &mut self,
1098 ) -> Option<tokio::sync::mpsc::UnboundedReceiver<JsonRpcMessage>> {
1099 self.message_rx.take()
1100 }
1101
1102 fn emulate_initialize_response(&self, request_id: &serde_json::Value) {
1103 let response = JsonRpcMessage::Response(JsonRpcResponse {
1104 jsonrpc: "2.0".to_string(),
1105 id: request_id.clone(),
1106 result: serde_json::json!({
1107 "protocolVersion": crate::core::constants::mcp_protocol_version(),
1108 "serverInfo": {
1109 "name": "Emulated-Stateless-Server",
1110 "version": "1.0.0"
1111 },
1112 "capabilities": {
1113 "tools": { "listChanged": true },
1114 "prompts": { "listChanged": true },
1115 "resources": { "subscribe": true, "listChanged": true }
1116 }
1117 }),
1118 });
1119 if let Some(ref tx) = self.message_tx {
1120 let _ = tx.send(response);
1121 }
1122 }
1123
1124 #[allow(clippy::too_many_arguments)]
1125 async fn event_loop(
1126 relay_pool: Arc<dyn RelayPoolTrait>,
1127 pending: ClientCorrelationStore,
1128 server_pubkey: PublicKey,
1129 tx: tokio::sync::mpsc::UnboundedSender<JsonRpcMessage>,
1130 encryption_mode: EncryptionMode,
1131 gift_wrap_mode: GiftWrapMode,
1132 discovered_caps: Arc<Mutex<PeerCapabilities>>,
1133 init_event: Arc<Mutex<Option<Event>>>,
1134 server_supports_ephemeral: Arc<AtomicBool>,
1135 seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
1136 oversized_receiver: Arc<Mutex<OversizedTransferReceiver>>,
1137 accept_waiters: Arc<Mutex<HashMap<String, oneshot::Sender<()>>>>,
1138 original_progress_tokens: Arc<Mutex<LruCache<String, serde_json::Value>>>,
1139 oversized_enabled: bool,
1140 open_stream_registry: Arc<AsyncMutex<OpenStreamRegistry>>,
1141 open_stream_control_progress: Arc<AtomicU64>,
1142 open_stream_enabled: bool,
1143 timeout: Duration,
1144 cancel: CancellationToken,
1145 ) {
1146 let mut notifications = relay_pool.notifications();
1147 let sweep_interval = (timeout / 2).clamp(Duration::from_secs(1), Duration::from_secs(30));
1149 let mut sweep_timer =
1150 tokio::time::interval_at(tokio::time::Instant::now() + sweep_interval, sweep_interval);
1151
1152 loop {
1153 tokio::select! {
1154 _ = cancel.cancelled() => {
1155 tracing::info!(
1156 target: LOG_TARGET,
1157 "Client event loop cancelled"
1158 );
1159 break;
1160 }
1161 result = notifications.recv() => {
1162 let notification = match result {
1163 Ok(n) => n,
1164 Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
1165 tracing::warn!(
1166 target: LOG_TARGET,
1167 skipped = n,
1168 "Relay broadcast lagged, skipping missed events"
1169 );
1170 continue;
1171 }
1172 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
1173 };
1174 Self::handle_notification(
1175 ¬ification,
1176 &pending,
1177 server_pubkey,
1178 &tx,
1179 encryption_mode,
1180 gift_wrap_mode,
1181 &discovered_caps,
1182 &init_event,
1183 &server_supports_ephemeral,
1184 &seen_gift_wrap_ids,
1185 &oversized_receiver,
1186 &accept_waiters,
1187 &original_progress_tokens,
1188 &open_stream_registry,
1189 &open_stream_control_progress,
1190 open_stream_enabled,
1191 &relay_pool,
1192 )
1193 .await;
1194 }
1195 _ = sweep_timer.tick() => {
1196 let swept = pending.sweep_expired(timeout).await;
1197 if swept > 0 {
1198 tracing::warn!(
1199 target: LOG_TARGET,
1200 swept,
1201 timeout_ms = timeout.as_millis() as u64,
1202 "Swept stale pending requests (rmcp handles timeout errors)"
1203 );
1204 }
1205 if oversized_enabled {
1212 let reaped = {
1213 let mut receiver = match oversized_receiver.lock() {
1214 Ok(g) => g,
1215 Err(p) => p.into_inner(),
1216 };
1217 receiver.remove_expired()
1218 };
1219 for token in reaped {
1220 tracing::warn!(
1221 target: LOG_TARGET,
1222 token = %token,
1223 "Oversized transfer reaped by watchdog"
1224 );
1225 }
1226 }
1227 if open_stream_enabled {
1230 let gift_wrap_kind = outbound_gift_wrap_kind(
1231 gift_wrap_mode,
1232 server_supports_ephemeral.load(Ordering::Relaxed),
1233 );
1234 Self::sweep_client_open_stream_sessions(
1235 &open_stream_registry,
1236 &open_stream_control_progress,
1237 &relay_pool,
1238 server_pubkey,
1239 encryption_mode,
1240 gift_wrap_kind,
1241 Instant::now(),
1242 )
1243 .await;
1244 }
1245 }
1246 }
1247 }
1248 }
1249
1250 fn get_client_capability_tags(&self) -> Vec<Tag> {
1254 let mut tags = Vec::new();
1255 if self.config.encryption_mode != EncryptionMode::Disabled {
1256 tags.push(Tag::custom(
1257 TagKind::Custom(tags::SUPPORT_ENCRYPTION.into()),
1258 Vec::<String>::new(),
1259 ));
1260 if self.config.gift_wrap_mode != GiftWrapMode::Persistent {
1261 tags.push(Tag::custom(
1262 TagKind::Custom(tags::SUPPORT_ENCRYPTION_EPHEMERAL.into()),
1263 Vec::<String>::new(),
1264 ));
1265 }
1266 }
1267 if self.config.oversized_transfer.enabled {
1269 tags.push(Tag::custom(
1270 TagKind::Custom(tags::SUPPORT_OVERSIZED_TRANSFER.into()),
1271 Vec::<String>::new(),
1272 ));
1273 }
1274 if self.config.open_stream.enabled {
1276 tags.push(Tag::custom(
1277 TagKind::Custom(tags::SUPPORT_OPEN_STREAM.into()),
1278 Vec::<String>::new(),
1279 ));
1280 }
1281 tags
1282 }
1283
1284 fn get_pending_client_discovery_tags(&self) -> Vec<Tag> {
1286 if self.has_sent_discovery_tags.load(Ordering::Relaxed) {
1287 vec![]
1288 } else {
1289 self.get_client_capability_tags()
1290 }
1291 }
1292
1293 fn learn_server_discovery(
1295 discovered_caps: &Mutex<PeerCapabilities>,
1296 init_event: &Mutex<Option<Event>>,
1297 event: &Event,
1298 ) {
1299 let tag_vec: Vec<Tag> = event.tags.clone().to_vec();
1300 let discovered = parse_discovered_peer_capabilities(&tag_vec);
1301 if discovered.discovery_tags.is_empty() {
1302 return;
1303 }
1304
1305 {
1306 let mut caps = match discovered_caps.lock() {
1307 Ok(g) => g,
1308 Err(p) => p.into_inner(),
1309 };
1310 caps.supports_encryption |= discovered.capabilities.supports_encryption;
1311 caps.supports_ephemeral_encryption |=
1312 discovered.capabilities.supports_ephemeral_encryption;
1313 caps.supports_oversized_transfer |= discovered.capabilities.supports_oversized_transfer;
1314 caps.supports_open_stream |= discovered.capabilities.supports_open_stream;
1316 }
1317
1318 let mut stored = match init_event.lock() {
1319 Ok(g) => g,
1320 Err(p) => p.into_inner(),
1321 };
1322 match stored.as_ref() {
1323 None => *stored = Some(event.clone()),
1325 Some(existing) => {
1332 if !Self::event_has_initialize_result(existing)
1333 && Self::event_has_initialize_result(event)
1334 {
1335 *stored = Some(event.clone());
1336 }
1337 }
1338 }
1339 }
1340
1341 fn event_has_initialize_result(event: &Event) -> bool {
1345 serde_json::from_str::<serde_json::Value>(&event.content)
1346 .ok()
1347 .as_ref()
1348 .and_then(|content| content.get("result"))
1349 .and_then(|result| result.get("protocolVersion"))
1350 .is_some()
1351 }
1352
1353 pub fn get_server_initialize_event(&self) -> Option<Event> {
1355 let guard = match self.server_initialize_event.lock() {
1356 Ok(g) => g,
1357 Err(p) => p.into_inner(),
1358 };
1359 guard.clone()
1360 }
1361
1362 pub fn discovered_server_capabilities(&self) -> PeerCapabilities {
1364 let guard = match self.discovered_server_capabilities.lock() {
1365 Ok(g) => g,
1366 Err(p) => p.into_inner(),
1367 };
1368 *guard
1369 }
1370
1371 pub fn prepare_outbound_open_stream_session(
1379 &self,
1380 ) -> oneshot::Receiver<Result<(String, OpenStreamSession)>> {
1381 let (tx, rx) = oneshot::channel();
1382 let mut pending = match self.pending_outbound_open_stream.lock() {
1383 Ok(g) => g,
1384 Err(p) => p.into_inner(),
1385 };
1386 pending.push_back(tx);
1387 rx
1388 }
1389
1390 async fn bind_pending_outbound_open_stream(&self, token: &str, original: &serde_json::Value) {
1398 let waiter = {
1399 let mut pending = match self.pending_outbound_open_stream.lock() {
1400 Ok(g) => g,
1401 Err(p) => p.into_inner(),
1402 };
1403 pending.pop_front()
1404 };
1405 if let Some(waiter) = waiter {
1406 self.record_original_progress_token(token, original);
1407 let result = self
1408 .create_outbound_open_stream_session(token)
1409 .await
1410 .map(|session| (token.to_string(), session));
1411 let _ = waiter.send(result);
1412 }
1413 }
1414
1415 async fn create_outbound_open_stream_session(&self, token: &str) -> Result<OpenStreamSession> {
1418 let mut registry = self.open_stream_registry.lock().await;
1419 if let Some(existing) = registry.get_session(token) {
1420 return Ok(existing);
1421 }
1422 let init = OpenStreamSessionInit {
1423 publish_frame: Some(self.open_stream_publish_closure()),
1424 ..Default::default()
1425 };
1426 Ok(registry.create_session_with(token, init)?)
1427 }
1428
1429 pub async fn get_open_stream_session(&self, token: &str) -> Option<OpenStreamSession> {
1431 self.open_stream_registry.lock().await.get_session(token)
1432 }
1433
1434 pub fn open_stream_handle(&self) -> ClientOpenStreamHandle {
1441 ClientOpenStreamHandle {
1442 registry: self.open_stream_registry.clone(),
1443 pending: self.pending_outbound_open_stream.clone(),
1444 bind_lock: self.open_stream_bind_lock.clone(),
1445 config: self.config.open_stream.clone(),
1446 }
1447 }
1448
1449 pub async fn abort_open_stream(&self, token: &str, reason: Option<String>) {
1453 let session = { self.open_stream_registry.lock().await.get_session(token) };
1454 if let Some(session) = session {
1455 session.abort(reason.clone()).await;
1457 }
1458 self.open_stream_registry
1460 .lock()
1461 .await
1462 .consumer_abort(token, reason)
1463 .await;
1464 }
1465
1466 fn open_stream_publish_closure(&self) -> PublishFrame {
1470 let relay_pool = Arc::clone(&self.base.relay_pool);
1471 let encryption_mode = self.config.encryption_mode;
1472 let server_pubkey = self.server_pubkey;
1473 let gift_wrap_kind = self.choose_outbound_gift_wrap_kind();
1474 Arc::new(move |notification: JsonRpcNotification| {
1475 let relay_pool = Arc::clone(&relay_pool);
1476 Box::pin(async move {
1477 let base = BaseTransport {
1478 relay_pool,
1479 encryption_mode,
1480 is_connected: true,
1481 };
1482 let tags = BaseTransport::create_recipient_tags(&server_pubkey);
1483 base.send_mcp_message(
1484 &JsonRpcMessage::Notification(notification),
1485 &server_pubkey,
1486 CTXVM_MESSAGES_KIND,
1487 tags,
1488 None,
1489 Some(gift_wrap_kind),
1490 )
1491 .await
1492 })
1493 })
1494 }
1495
1496 #[allow(clippy::too_many_arguments)]
1501 async fn handle_inbound_open_stream_frame(
1502 open_stream_registry: &Arc<AsyncMutex<OpenStreamRegistry>>,
1503 open_stream_control_progress: &Arc<AtomicU64>,
1504 original_progress_tokens: &Mutex<LruCache<String, serde_json::Value>>,
1505 pending: &ClientCorrelationStore,
1506 relay_pool: &Arc<dyn RelayPoolTrait>,
1507 server_pubkey: PublicKey,
1508 encryption_mode: EncryptionMode,
1509 gift_wrap_kind: u16,
1510 tx: &tokio::sync::mpsc::UnboundedSender<JsonRpcMessage>,
1511 notif: &JsonRpcNotification,
1512 e_tag: Option<&str>,
1513 ) {
1514 let token = notif
1515 .params
1516 .as_ref()
1517 .and_then(|p| p.get("progressToken"))
1518 .and_then(progress_token_string);
1519
1520 if let Some(correlated) = e_tag {
1522 pending.touch(correlated).await;
1523 }
1524
1525 let outcome = {
1527 open_stream_registry
1528 .lock()
1529 .await
1530 .process_frame(Instant::now(), notif)
1531 .await
1532 };
1533
1534 if let Ok(FrameOutcome::SendPong(nonce)) = &outcome {
1536 if let Some(token) = token.as_deref() {
1537 let progress = open_stream_control_progress.fetch_add(1, Ordering::SeqCst) + 1;
1538 if let Ok(frame) = (OpenStreamFrame::Pong {
1539 nonce: nonce.clone(),
1540 })
1541 .into_progress_notification(token, progress, None)
1542 {
1543 let base = BaseTransport {
1544 relay_pool: Arc::clone(relay_pool),
1545 encryption_mode,
1546 is_connected: true,
1547 };
1548 let tags = BaseTransport::create_recipient_tags(&server_pubkey);
1549 let _ = base
1550 .send_mcp_message(
1551 &JsonRpcMessage::Notification(frame),
1552 &server_pubkey,
1553 CTXVM_MESSAGES_KIND,
1554 tags,
1555 None,
1556 Some(gift_wrap_kind),
1557 )
1558 .await;
1559 }
1560 }
1561 }
1562
1563 if let Some(token) = token.as_deref() {
1567 Self::forward_stripped_progress(notif, token, original_progress_tokens, tx);
1568 }
1569
1570 if matches!(
1572 &outcome,
1573 Ok(FrameOutcome::Closed) | Ok(FrameOutcome::Aborted(_)) | Err(_)
1574 ) {
1575 Self::remove_original_progress_token(original_progress_tokens, token.as_deref());
1576 }
1577 }
1578
1579 async fn sweep_client_open_stream_sessions(
1584 open_stream_registry: &Arc<AsyncMutex<OpenStreamRegistry>>,
1585 open_stream_control_progress: &Arc<AtomicU64>,
1586 relay_pool: &Arc<dyn RelayPoolTrait>,
1587 server_pubkey: PublicKey,
1588 encryption_mode: EncryptionMode,
1589 gift_wrap_kind: u16,
1590 now: Instant,
1591 ) {
1592 let actions = { open_stream_registry.lock().await.tick_all(now) };
1593 for (token, action) in actions {
1594 if let KeepaliveAction::SendPing(nonce) = action {
1595 let progress = open_stream_control_progress.fetch_add(1, Ordering::SeqCst) + 1;
1596 if let Ok(frame) = (OpenStreamFrame::Ping { nonce })
1597 .into_progress_notification(&token, progress, None)
1598 {
1599 let base = BaseTransport {
1600 relay_pool: Arc::clone(relay_pool),
1601 encryption_mode,
1602 is_connected: true,
1603 };
1604 let tags = BaseTransport::create_recipient_tags(&server_pubkey);
1605 let _ = base
1606 .send_mcp_message(
1607 &JsonRpcMessage::Notification(frame),
1608 &server_pubkey,
1609 CTXVM_MESSAGES_KIND,
1610 tags,
1611 None,
1612 Some(gift_wrap_kind),
1613 )
1614 .await;
1615 }
1616 }
1617 }
1618 }
1619
1620 pub async fn run_open_stream_keepalive_sweep(&self, now: Instant) {
1626 Self::sweep_client_open_stream_sessions(
1627 &self.open_stream_registry,
1628 &self.open_stream_control_progress,
1629 &self.base.relay_pool,
1630 self.server_pubkey,
1631 self.config.encryption_mode,
1632 self.choose_outbound_gift_wrap_kind(),
1633 now,
1634 )
1635 .await;
1636 }
1637
1638 #[allow(clippy::too_many_arguments)]
1639 async fn handle_notification(
1640 notification: &RelayPoolNotification,
1641 pending: &ClientCorrelationStore,
1642 server_pubkey: PublicKey,
1643 tx: &tokio::sync::mpsc::UnboundedSender<JsonRpcMessage>,
1644 encryption_mode: EncryptionMode,
1645 gift_wrap_mode: GiftWrapMode,
1646 discovered_caps: &Arc<Mutex<PeerCapabilities>>,
1647 init_event: &Arc<Mutex<Option<Event>>>,
1648 server_supports_ephemeral: &Arc<AtomicBool>,
1649 seen_gift_wrap_ids: &Arc<Mutex<LruCache<EventId, ()>>>,
1650 oversized_receiver: &Arc<Mutex<OversizedTransferReceiver>>,
1651 accept_waiters: &Arc<Mutex<HashMap<String, oneshot::Sender<()>>>>,
1652 original_progress_tokens: &Arc<Mutex<LruCache<String, serde_json::Value>>>,
1653 open_stream_registry: &Arc<AsyncMutex<OpenStreamRegistry>>,
1654 open_stream_control_progress: &Arc<AtomicU64>,
1655 open_stream_enabled: bool,
1656 relay_pool: &Arc<dyn RelayPoolTrait>,
1657 ) {
1658 let event = match notification {
1659 RelayPoolNotification::Event { event, .. } => event,
1660 _ => return,
1661 };
1662
1663 let is_gift_wrap = is_gift_wrap_kind(&event.kind);
1664 let outer_kind = event.kind.as_u16();
1665
1666 if violates_encryption_policy(&event.kind, &encryption_mode) {
1668 if is_gift_wrap {
1669 tracing::warn!(
1670 target: LOG_TARGET,
1671 event_id = %event.id.to_hex(),
1672 event_kind = outer_kind,
1673 configured_mode = ?gift_wrap_mode,
1674 "Skipping encrypted response because client encryption is disabled"
1675 );
1676 } else {
1677 tracing::warn!(
1678 target: LOG_TARGET,
1679 event_id = %event.id.to_hex(),
1680 "Skipping plaintext response because client encryption is required"
1681 );
1682 }
1683 return;
1684 }
1685
1686 if is_gift_wrap && !gift_wrap_mode.allows_kind(outer_kind) {
1688 tracing::warn!(
1689 target: LOG_TARGET,
1690 event_id = %event.id.to_hex(),
1691 event_kind = outer_kind,
1692 configured_mode = ?gift_wrap_mode,
1693 "Skipping gift wrap due to CEP-19 policy"
1694 );
1695 return;
1696 }
1697
1698 let (actual_event_content, actual_pubkey, e_tag, verified_tags, source_event) =
1700 if is_gift_wrap {
1701 {
1702 let guard = match seen_gift_wrap_ids.lock() {
1703 Ok(g) => g,
1704 Err(poisoned) => poisoned.into_inner(),
1705 };
1706 if guard.contains(&event.id) {
1707 tracing::debug!(
1708 target: LOG_TARGET,
1709 event_id = %event.id.to_hex(),
1710 "Skipping duplicate gift-wrap (outer id)"
1711 );
1712 return;
1713 }
1714 }
1715 let signer = match relay_pool.signer().await {
1717 Ok(s) => s,
1718 Err(error) => {
1719 tracing::error!(
1720 target: LOG_TARGET,
1721 error = %error,
1722 "Failed to get signer"
1723 );
1724 return;
1725 }
1726 };
1727 match encryption::decrypt_gift_wrap_single_layer(&signer, event).await {
1728 Ok(decrypted_json) => match serde_json::from_str::<Event>(&decrypted_json) {
1729 Ok(inner) => {
1730 if let Err(e) = inner.verify() {
1731 tracing::warn!("Inner event signature verification failed: {e}");
1732 return;
1733 }
1734 {
1735 let mut guard = match seen_gift_wrap_ids.lock() {
1736 Ok(g) => g,
1737 Err(poisoned) => poisoned.into_inner(),
1738 };
1739 guard.put(event.id, ());
1740 }
1741 let e_tag = serializers::get_tag_value(&inner.tags, "e");
1742 let inner_clone = inner.clone();
1743 (inner.content, inner.pubkey, e_tag, inner.tags, inner_clone)
1744 }
1745 Err(error) => {
1746 tracing::error!(
1747 target: LOG_TARGET,
1748 error = %error,
1749 "Failed to parse inner event"
1750 );
1751 return;
1752 }
1753 },
1754 Err(error) => {
1755 tracing::error!(
1756 target: LOG_TARGET,
1757 error = %error,
1758 "Failed to decrypt gift wrap"
1759 );
1760 return;
1761 }
1762 }
1763 } else {
1764 let e_tag = serializers::get_tag_value(&event.tags, "e");
1765 let event_clone: Event = (**event).clone();
1766 (
1767 event.content.clone(),
1768 event.pubkey,
1769 e_tag,
1770 event.tags.clone(),
1771 event_clone,
1772 )
1773 };
1774
1775 if actual_pubkey != server_pubkey {
1777 tracing::debug!(
1778 target: LOG_TARGET,
1779 event_pubkey = %actual_pubkey.to_hex(),
1780 expected_pubkey = %server_pubkey.to_hex(),
1781 "Skipping event from unexpected pubkey"
1782 );
1783 return;
1784 }
1785
1786 Self::learn_server_discovery(discovered_caps, init_event, &source_event);
1788
1789 if Self::should_learn_ephemeral_support(
1791 actual_pubkey,
1792 server_pubkey,
1793 if is_gift_wrap { Some(outer_kind) } else { None },
1794 &verified_tags,
1795 ) {
1796 server_supports_ephemeral.store(true, Ordering::Relaxed);
1797 }
1798
1799 if open_stream_enabled {
1805 if let Ok(notif) = serde_json::from_str::<JsonRpcNotification>(&actual_event_content) {
1806 if notif.method == NOTIFICATIONS_PROGRESS_METHOD
1807 && OpenStreamReceiver::is_open_stream_frame(¬if)
1808 {
1809 let gift_wrap_kind = outbound_gift_wrap_kind(
1810 gift_wrap_mode,
1811 server_supports_ephemeral.load(Ordering::Relaxed),
1812 );
1813 Self::handle_inbound_open_stream_frame(
1814 open_stream_registry,
1815 open_stream_control_progress,
1816 original_progress_tokens,
1817 pending,
1818 relay_pool,
1819 server_pubkey,
1820 encryption_mode,
1821 gift_wrap_kind,
1822 tx,
1823 ¬if,
1824 e_tag.as_deref(),
1825 )
1826 .await;
1827 return;
1828 }
1829 }
1830 }
1831
1832 if let Ok(notif) = serde_json::from_str::<JsonRpcNotification>(&actual_event_content) {
1838 if notif.method == NOTIFICATIONS_PROGRESS_METHOD
1839 && OversizedTransferReceiver::is_oversized_frame(¬if)
1840 {
1841 let token = notif
1844 .params
1845 .as_ref()
1846 .and_then(|p| p.get("progressToken"))
1847 .and_then(progress_token_string);
1848
1849 let is_accept = notif
1852 .params
1853 .as_ref()
1854 .and_then(|p| p.get("cvm"))
1855 .and_then(OversizedFrame::from_cvm_value)
1856 .is_some_and(|f| matches!(f, OversizedFrame::Accept));
1857 if is_accept {
1858 if let Some(ref token) = token {
1859 let waiter = {
1860 let mut waiters = match accept_waiters.lock() {
1861 Ok(g) => g,
1862 Err(p) => p.into_inner(),
1863 };
1864 waiters.remove(token)
1865 };
1866 if let Some(waiter) = waiter {
1867 let _ = waiter.send(());
1868 Self::forward_stripped_progress(
1874 ¬if,
1875 token,
1876 original_progress_tokens,
1877 tx,
1878 );
1879 }
1880 }
1881 return;
1882 }
1883
1884 if let Some(ref correlated_id) = e_tag {
1887 pending.touch(correlated_id.as_str()).await;
1888 }
1889
1890 let (outcome, tracked) = {
1893 let mut receiver = match oversized_receiver.lock() {
1894 Ok(g) => g,
1895 Err(p) => p.into_inner(),
1896 };
1897 let outcome = receiver.process_frame(¬if);
1898 let tracked = token
1902 .as_deref()
1903 .is_some_and(|token| receiver.is_tracking(token));
1904 (outcome, tracked)
1905 };
1906 match outcome {
1907 Ok(None) => {
1911 if tracked {
1912 if let Some(ref token) = token {
1913 Self::forward_stripped_progress(
1914 ¬if,
1915 token,
1916 original_progress_tokens,
1917 tx,
1918 );
1919 }
1920 }
1921 return;
1922 }
1923 Ok(Some(message)) => {
1927 if let Some(ref correlated_id) = e_tag {
1928 pending.remove(correlated_id.as_str()).await;
1929 } else {
1930 tracing::warn!(
1935 target: LOG_TARGET,
1936 "Oversized transfer completed without a correlation `e` tag; \
1937 delivering the reassembled response uncorrelated"
1938 );
1939 }
1940 Self::remove_original_progress_token(
1941 original_progress_tokens,
1942 token.as_deref(),
1943 );
1944 let _ = tx.send(message);
1945 return;
1946 }
1947 Err(error) => {
1949 tracing::warn!(
1950 target: LOG_TARGET,
1951 error = %error,
1952 "Inbound oversized transfer failed"
1953 );
1954 Self::remove_original_progress_token(
1955 original_progress_tokens,
1956 token.as_deref(),
1957 );
1958 return;
1959 }
1960 }
1961 }
1962 }
1963
1964 if let Some(ref correlated_id) = e_tag {
1966 let is_pending = pending.contains(correlated_id.as_str()).await;
1967 if !is_pending {
1968 tracing::warn!(
1969 target: LOG_TARGET,
1970 correlated_event_id = %correlated_id,
1971 "Response for unknown request"
1972 );
1973 return;
1974 }
1975 }
1976
1977 if let Some(mcp_msg) = validation::validate_and_parse(&actual_event_content) {
1979 match &mcp_msg {
1981 JsonRpcMessage::Response(_) | JsonRpcMessage::ErrorResponse(_)
1982 if e_tag.is_none() =>
1983 {
1984 tracing::warn!(
1985 target: LOG_TARGET,
1986 "Dropping response/error without correlation `e` tag"
1987 );
1988 return;
1989 }
1990 JsonRpcMessage::Request(_) => {
1991 tracing::warn!(
1992 target: LOG_TARGET,
1993 method = ?mcp_msg.method(),
1994 "Dropping server-to-client request (invalid in MCP)"
1995 );
1996 return;
1997 }
1998 _ => {}
1999 }
2000
2001 if let Some(ref correlated_id) = e_tag {
2003 pending.remove(correlated_id.as_str()).await;
2004 }
2005 let _ = tx.send(mcp_msg);
2006 }
2007 }
2008
2009 fn choose_outbound_gift_wrap_kind(&self) -> u16 {
2010 outbound_gift_wrap_kind(
2011 self.config.gift_wrap_mode,
2012 self.server_supports_ephemeral.load(Ordering::Relaxed),
2013 )
2014 }
2015
2016 fn has_support_ephemeral_tag(tags: &Tags) -> bool {
2017 tags.iter().any(|tag| {
2018 tag.kind()
2019 == TagKind::Custom(
2020 crate::core::constants::tags::SUPPORT_ENCRYPTION_EPHEMERAL.into(),
2021 )
2022 })
2023 }
2024
2025 fn should_learn_ephemeral_support(
2026 actual_pubkey: PublicKey,
2027 server_pubkey: PublicKey,
2028 event_kind: Option<u16>,
2029 tags: &Tags,
2030 ) -> bool {
2031 actual_pubkey == server_pubkey
2032 && (event_kind == Some(EPHEMERAL_GIFT_WRAP_KIND)
2033 || Self::has_support_ephemeral_tag(tags))
2034 }
2035
2036 pub fn server_supports_ephemeral_encryption(&self) -> bool {
2038 self.server_supports_ephemeral.load(Ordering::Relaxed)
2039 }
2040}
2041
2042#[inline]
2043fn is_gift_wrap_kind(kind: &Kind) -> bool {
2044 *kind == Kind::Custom(GIFT_WRAP_KIND) || *kind == Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND)
2045}
2046
2047#[inline]
2050fn outbound_gift_wrap_kind(mode: GiftWrapMode, server_supports_ephemeral: bool) -> u16 {
2051 match mode {
2052 GiftWrapMode::Persistent => GIFT_WRAP_KIND,
2053 GiftWrapMode::Ephemeral => EPHEMERAL_GIFT_WRAP_KIND,
2054 GiftWrapMode::Optional => {
2055 if server_supports_ephemeral {
2056 EPHEMERAL_GIFT_WRAP_KIND
2057 } else {
2058 GIFT_WRAP_KIND
2059 }
2060 }
2061 }
2062}
2063
2064#[inline]
2067fn violates_encryption_policy(kind: &Kind, mode: &EncryptionMode) -> bool {
2068 let is_gift_wrap = is_gift_wrap_kind(kind);
2069 (is_gift_wrap && *mode == EncryptionMode::Disabled)
2070 || (!is_gift_wrap && *mode == EncryptionMode::Required)
2071}
2072
2073#[cfg(test)]
2074mod tests {
2075 use super::*;
2076
2077 #[test]
2078 fn test_config_defaults() {
2079 let config = NostrClientTransportConfig::default();
2080 assert!(config.relay_urls.is_empty());
2081 assert!(config.server_pubkey.is_empty());
2082 assert_eq!(config.encryption_mode, EncryptionMode::Optional);
2083 assert_eq!(config.gift_wrap_mode, GiftWrapMode::Optional);
2084 assert!(!config.is_stateless);
2085 assert_eq!(config.timeout, Duration::from_secs(30));
2086 assert!(config.discovery_relay_urls.is_none());
2087 assert!(config.fallback_operational_relay_urls.is_none());
2088 }
2089
2090 #[test]
2091 fn test_stateless_config() {
2092 let config = NostrClientTransportConfig {
2093 is_stateless: true,
2094 ..Default::default()
2095 };
2096 assert!(config.is_stateless);
2097 }
2098
2099 #[test]
2100 fn test_custom_timeout_config() {
2101 let config = NostrClientTransportConfig {
2102 timeout: Duration::from_secs(60),
2103 ..Default::default()
2104 };
2105 assert_eq!(config.timeout, Duration::from_secs(60));
2106 }
2107
2108 #[test]
2109 fn test_has_support_ephemeral_tag_detects_capability() {
2110 let tags = Tags::from_list(vec![Tag::custom(
2111 TagKind::Custom(crate::core::constants::tags::SUPPORT_ENCRYPTION_EPHEMERAL.into()),
2112 Vec::<String>::new(),
2113 )]);
2114 assert!(NostrClientTransport::has_support_ephemeral_tag(&tags));
2115 }
2116
2117 #[test]
2118 fn test_has_support_ephemeral_tag_absent() {
2119 let tags = Tags::from_list(vec![Tag::custom(
2120 TagKind::Custom(crate::core::constants::tags::SUPPORT_ENCRYPTION.into()),
2121 Vec::<String>::new(),
2122 )]);
2123 assert!(!NostrClientTransport::has_support_ephemeral_tag(&tags));
2124 }
2125
2126 #[test]
2127 fn test_should_learn_ephemeral_support_requires_matching_server_pubkey() {
2128 let server_keys = Keys::generate();
2129 let other_keys = Keys::generate();
2130 let tags = Tags::from_list(vec![Tag::custom(
2131 TagKind::Custom(crate::core::constants::tags::SUPPORT_ENCRYPTION_EPHEMERAL.into()),
2132 Vec::<String>::new(),
2133 )]);
2134
2135 assert!(!NostrClientTransport::should_learn_ephemeral_support(
2136 other_keys.public_key(),
2137 server_keys.public_key(),
2138 Some(EPHEMERAL_GIFT_WRAP_KIND),
2139 &tags,
2140 ));
2141 assert!(NostrClientTransport::should_learn_ephemeral_support(
2142 server_keys.public_key(),
2143 server_keys.public_key(),
2144 Some(EPHEMERAL_GIFT_WRAP_KIND),
2145 &tags,
2146 ));
2147 }
2148
2149 #[test]
2150 fn test_should_learn_from_ephemeral_kind_even_without_tag() {
2151 let server_keys = Keys::generate();
2152 let empty_tags = Tags::from_list(vec![]);
2153
2154 assert!(NostrClientTransport::should_learn_ephemeral_support(
2155 server_keys.public_key(),
2156 server_keys.public_key(),
2157 Some(EPHEMERAL_GIFT_WRAP_KIND),
2158 &empty_tags,
2159 ));
2160 }
2161
2162 #[test]
2163 fn test_should_learn_from_tag_without_ephemeral_kind() {
2164 let server_keys = Keys::generate();
2165 let tags = Tags::from_list(vec![Tag::custom(
2166 TagKind::Custom(crate::core::constants::tags::SUPPORT_ENCRYPTION_EPHEMERAL.into()),
2167 Vec::<String>::new(),
2168 )]);
2169
2170 assert!(NostrClientTransport::should_learn_ephemeral_support(
2171 server_keys.public_key(),
2172 server_keys.public_key(),
2173 Some(GIFT_WRAP_KIND), &tags,
2175 ));
2176 }
2177
2178 #[test]
2179 fn test_stateless_emulated_initialize_response_shape() {
2180 let request_id = serde_json::json!(1);
2181 let response = JsonRpcMessage::Response(JsonRpcResponse {
2182 jsonrpc: "2.0".to_string(),
2183 id: request_id.clone(),
2184 result: serde_json::json!({
2185 "protocolVersion": crate::core::constants::mcp_protocol_version(),
2186 "serverInfo": {
2187 "name": "Emulated-Stateless-Server",
2188 "version": "1.0.0"
2189 },
2190 "capabilities": {
2191 "tools": { "listChanged": true },
2192 "prompts": { "listChanged": true },
2193 "resources": { "subscribe": true, "listChanged": true }
2194 }
2195 }),
2196 });
2197 assert!(response.is_response());
2198 assert_eq!(response.id(), Some(&serde_json::json!(1)));
2199
2200 if let JsonRpcMessage::Response(r) = &response {
2201 assert!(r.result.get("capabilities").is_some());
2202 assert!(r.result.get("serverInfo").is_some());
2203 let server_info = r.result.get("serverInfo").unwrap();
2204 assert_eq!(
2205 server_info.get("name").unwrap().as_str().unwrap(),
2206 "Emulated-Stateless-Server"
2207 );
2208 }
2209 }
2210
2211 #[test]
2212 fn test_stateless_mode_initialize_request_detection() {
2213 let init_req = JsonRpcMessage::Request(JsonRpcRequest {
2214 jsonrpc: "2.0".to_string(),
2215 id: serde_json::json!(1),
2216 method: "initialize".to_string(),
2217 params: None,
2218 });
2219 assert_eq!(init_req.method(), Some("initialize"));
2220
2221 let init_notif = JsonRpcMessage::Notification(JsonRpcNotification {
2222 jsonrpc: "2.0".to_string(),
2223 method: "notifications/initialized".to_string(),
2224 params: None,
2225 });
2226 assert_eq!(init_notif.method(), Some("notifications/initialized"));
2227 }
2228
2229 #[test]
2230 fn test_gift_wrap_kind_detection() {
2231 assert!(is_gift_wrap_kind(&Kind::Custom(GIFT_WRAP_KIND)));
2232 assert!(is_gift_wrap_kind(&Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND)));
2233 assert!(!is_gift_wrap_kind(&Kind::Custom(CTXVM_MESSAGES_KIND)));
2234 }
2235
2236 #[test]
2237 fn test_required_mode_drops_plaintext() {
2238 let plaintext_kind = Kind::Custom(CTXVM_MESSAGES_KIND);
2239 assert!(
2240 violates_encryption_policy(&plaintext_kind, &EncryptionMode::Required),
2241 "Required mode must reject plaintext (non-gift-wrap) events"
2242 );
2243 }
2244
2245 #[test]
2246 fn test_disabled_mode_drops_encrypted() {
2247 assert!(
2248 violates_encryption_policy(&Kind::Custom(GIFT_WRAP_KIND), &EncryptionMode::Disabled),
2249 "Disabled mode must reject gift-wrap events"
2250 );
2251 assert!(
2252 violates_encryption_policy(
2253 &Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND),
2254 &EncryptionMode::Disabled
2255 ),
2256 "Disabled mode must reject ephemeral gift-wrap events"
2257 );
2258 }
2259
2260 #[test]
2261 fn test_optional_mode_accepts_all() {
2262 let plaintext = Kind::Custom(CTXVM_MESSAGES_KIND);
2263 let gift_wrap = Kind::Custom(GIFT_WRAP_KIND);
2264 let ephemeral = Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND);
2265 assert!(!violates_encryption_policy(
2266 &plaintext,
2267 &EncryptionMode::Optional
2268 ));
2269 assert!(!violates_encryption_policy(
2270 &gift_wrap,
2271 &EncryptionMode::Optional
2272 ));
2273 assert!(!violates_encryption_policy(
2274 &ephemeral,
2275 &EncryptionMode::Optional
2276 ));
2277 }
2278
2279 #[test]
2280 fn test_required_mode_accepts_encrypted() {
2281 assert!(
2282 !violates_encryption_policy(&Kind::Custom(GIFT_WRAP_KIND), &EncryptionMode::Required),
2283 "Required mode must accept gift-wrap events"
2284 );
2285 assert!(
2286 !violates_encryption_policy(
2287 &Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND),
2288 &EncryptionMode::Required
2289 ),
2290 "Required mode must accept ephemeral gift-wrap events"
2291 );
2292 }
2293
2294 #[test]
2295 fn test_disabled_mode_accepts_plaintext() {
2296 let plaintext = Kind::Custom(CTXVM_MESSAGES_KIND);
2297 assert!(
2298 !violates_encryption_policy(&plaintext, &EncryptionMode::Disabled),
2299 "Disabled mode must accept plaintext events"
2300 );
2301 }
2302
2303 fn make_transport_for_tags(
2306 encryption_mode: EncryptionMode,
2307 gift_wrap_mode: GiftWrapMode,
2308 ) -> NostrClientTransport {
2309 let keys = Keys::generate();
2310 NostrClientTransport {
2311 base: BaseTransport {
2312 relay_pool: Arc::new(crate::relay::mock::MockRelayPool::new()),
2313 encryption_mode,
2314 is_connected: false,
2315 },
2316 config: NostrClientTransportConfig {
2317 encryption_mode,
2318 gift_wrap_mode,
2319 server_pubkey: Keys::generate().public_key().to_hex(),
2320 ..Default::default()
2321 },
2322 server_pubkey: keys.public_key(),
2323 hinted_relay_urls: vec![],
2324 discovery_relay_urls: vec![],
2325 fallback_operational_relay_urls: vec![],
2326 pending_requests: ClientCorrelationStore::new(),
2327 has_sent_discovery_tags: AtomicBool::new(false),
2328 discovered_server_capabilities: Arc::new(Mutex::new(PeerCapabilities::default())),
2329 server_initialize_event: Arc::new(Mutex::new(None)),
2330 server_supports_ephemeral: Arc::new(AtomicBool::new(false)),
2331 seen_gift_wrap_ids: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(10).unwrap()))),
2332 oversized_receiver: Arc::new(Mutex::new(OversizedTransferReceiver::new())),
2333 accept_waiters: Arc::new(Mutex::new(HashMap::new())),
2334 original_progress_tokens: Arc::new(Mutex::new(LruCache::new(
2335 NonZeroUsize::new(10).unwrap(),
2336 ))),
2337 open_stream_registry: Arc::new(AsyncMutex::new(OpenStreamRegistry::new())),
2338 pending_outbound_open_stream: Arc::new(Mutex::new(VecDeque::new())),
2339 open_stream_control_progress: Arc::new(AtomicU64::new(0)),
2340 open_stream_bind_lock: Arc::new(AsyncMutex::new(())),
2341 message_tx: Some(tokio::sync::mpsc::unbounded_channel().0),
2342 message_rx: None,
2343 cancellation_token: CancellationToken::new(),
2344 event_loop_handle: None,
2345 }
2346 }
2347
2348 fn make_tag(parts: &[&str]) -> Tag {
2349 let kind = TagKind::Custom(parts[0].into());
2350 let values: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
2351 Tag::custom(kind, values)
2352 }
2353
2354 fn tag_names(tags: &[Tag]) -> Vec<String> {
2355 tags.iter().map(|t| t.clone().to_vec()[0].clone()).collect()
2356 }
2357
2358 #[test]
2359 fn client_capability_tags_encryption_optional() {
2360 let t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
2361 let tags = t.get_client_capability_tags();
2362 let names = tag_names(&tags);
2363 assert_eq!(
2365 names,
2366 vec![
2367 "support_encryption",
2368 "support_encryption_ephemeral",
2369 "support_oversized_transfer"
2370 ]
2371 );
2372 }
2373
2374 #[test]
2375 fn client_capability_tags_encryption_disabled() {
2376 let t = make_transport_for_tags(EncryptionMode::Disabled, GiftWrapMode::Optional);
2377 let tags = t.get_client_capability_tags();
2378 assert_eq!(tag_names(&tags), vec!["support_oversized_transfer"]);
2380 }
2381
2382 #[test]
2383 fn client_capability_tags_persistent_gift_wrap() {
2384 let t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Persistent);
2385 let tags = t.get_client_capability_tags();
2386 let names = tag_names(&tags);
2387 assert_eq!(
2388 names,
2389 vec!["support_encryption", "support_oversized_transfer"]
2390 );
2391 }
2392
2393 #[test]
2394 fn client_capability_tags_oversized_enabled_by_default() {
2395 let t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
2396 assert!(t.config.oversized_transfer.enabled);
2397 let names = tag_names(&t.get_client_capability_tags());
2398 assert!(
2399 names.contains(&"support_oversized_transfer".to_string()),
2400 "oversized tag must be advertised by default"
2401 );
2402 }
2403
2404 #[test]
2405 fn client_capability_tags_oversized_opt_out() {
2406 let mut t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
2408 t.config.oversized_transfer = OversizedTransferConfig::default().with_enabled(false);
2409 let names = tag_names(&t.get_client_capability_tags());
2410 assert!(
2411 !names.contains(&"support_oversized_transfer".to_string()),
2412 "oversized tag must not be advertised when disabled"
2413 );
2414 }
2415
2416 #[test]
2417 fn client_capability_tags_oversized_enabled() {
2418 let mut t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
2419 t.config.oversized_transfer.enabled = true;
2420 let names = tag_names(&t.get_client_capability_tags());
2421 assert!(
2422 names.contains(&"support_oversized_transfer".to_string()),
2423 "oversized tag must be advertised when enabled"
2424 );
2425 }
2426
2427 #[test]
2428 fn client_capability_tags_oversized_enabled_without_encryption() {
2429 let mut t = make_transport_for_tags(EncryptionMode::Disabled, GiftWrapMode::Optional);
2431 t.config.oversized_transfer.enabled = true;
2432 let names = tag_names(&t.get_client_capability_tags());
2433 assert_eq!(names, vec!["support_oversized_transfer"]);
2434 }
2435
2436 #[test]
2437 fn client_capability_tags_open_stream_gate() {
2438 let mut t = make_transport_for_tags(EncryptionMode::Disabled, GiftWrapMode::Optional);
2440 assert!(
2441 !tag_names(&t.get_client_capability_tags())
2442 .contains(&"support_open_stream".to_string()),
2443 "open-stream tag must be absent by default (opt-in)"
2444 );
2445 t.config.open_stream = OpenStreamConfig::enabled();
2447 assert!(
2448 tag_names(&t.get_client_capability_tags()).contains(&"support_open_stream".to_string()),
2449 "open-stream tag must be advertised when enabled"
2450 );
2451 }
2452
2453 #[test]
2454 fn client_learn_server_discovery_learns_open_stream() {
2455 let caps = Mutex::new(PeerCapabilities::default());
2456 let init = Mutex::new(None);
2457 let event = make_event_with_tags(&[&["support_open_stream"]]);
2458 NostrClientTransport::learn_server_discovery(&caps, &init, &event);
2459 assert!(caps.lock().unwrap().supports_open_stream);
2460 }
2461
2462 #[test]
2463 fn client_config_oversized_builders() {
2464 let cfg = NostrClientTransportConfig::default().with_oversized_enabled(true);
2465 assert!(cfg.oversized_transfer.enabled);
2466 let cfg = NostrClientTransportConfig::default()
2467 .with_oversized_transfer(OversizedTransferConfig::enabled().with_chunk_size(1024));
2468 assert!(cfg.oversized_transfer.enabled);
2469 assert_eq!(cfg.oversized_transfer.chunk_size, 1024);
2470 }
2471
2472 #[test]
2475 fn original_progress_token_roundtrip_preserves_numeric_type() {
2476 let t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
2477 t.record_original_progress_token("7", &serde_json::json!(7));
2479 let restored = NostrClientTransport::remove_original_progress_token(
2480 &t.original_progress_tokens,
2481 Some("7"),
2482 );
2483 assert_eq!(restored, Some(serde_json::json!(7)));
2484 assert_eq!(
2486 NostrClientTransport::remove_original_progress_token(
2487 &t.original_progress_tokens,
2488 Some("7"),
2489 ),
2490 None
2491 );
2492 }
2493
2494 #[test]
2495 fn original_progress_token_string_never_parsed_to_number() {
2496 let t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
2499 t.record_original_progress_token("5", &serde_json::json!("5"));
2500 assert_eq!(
2501 NostrClientTransport::remove_original_progress_token(
2502 &t.original_progress_tokens,
2503 Some("5"),
2504 ),
2505 Some(serde_json::json!("5"))
2506 );
2507 }
2508
2509 #[test]
2510 fn remove_original_progress_token_handles_missing() {
2511 let t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
2512 assert_eq!(
2513 NostrClientTransport::remove_original_progress_token(&t.original_progress_tokens, None,),
2514 None
2515 );
2516 assert_eq!(
2517 NostrClientTransport::remove_original_progress_token(
2518 &t.original_progress_tokens,
2519 Some("unknown"),
2520 ),
2521 None
2522 );
2523 }
2524
2525 #[tokio::test]
2529 async fn send_records_numeric_progress_token_original() {
2530 let mut t = make_transport_for_tags(EncryptionMode::Disabled, GiftWrapMode::Optional);
2531 t.config.oversized_transfer.enabled = true;
2532 let request = JsonRpcMessage::Request(JsonRpcRequest {
2533 jsonrpc: "2.0".to_string(),
2534 id: serde_json::json!(1),
2535 method: "tools/call".to_string(),
2536 params: Some(serde_json::json!({ "_meta": { "progressToken": 7 } })),
2537 });
2538 t.send(&request).await.expect("send small request");
2539
2540 let recorded = NostrClientTransport::remove_original_progress_token(
2541 &t.original_progress_tokens,
2542 Some("7"),
2543 );
2544 assert_eq!(
2545 recorded,
2546 Some(serde_json::json!(7)),
2547 "numeric token must be recorded under its stringified form"
2548 );
2549 }
2550
2551 #[tokio::test]
2553 async fn send_records_nothing_when_oversized_disabled() {
2554 let mut t = make_transport_for_tags(EncryptionMode::Disabled, GiftWrapMode::Optional);
2555 t.config.oversized_transfer = OversizedTransferConfig::default().with_enabled(false);
2556 let request = JsonRpcMessage::Request(JsonRpcRequest {
2557 jsonrpc: "2.0".to_string(),
2558 id: serde_json::json!(1),
2559 method: "tools/call".to_string(),
2560 params: Some(serde_json::json!({ "_meta": { "progressToken": 7 } })),
2561 });
2562 t.send(&request).await.expect("send small request");
2563
2564 assert_eq!(
2565 NostrClientTransport::remove_original_progress_token(
2566 &t.original_progress_tokens,
2567 Some("7"),
2568 ),
2569 None
2570 );
2571 }
2572
2573 #[test]
2576 fn stripped_progress_notification_strips_cvm_and_restores_token() {
2577 let params = serde_json::json!({
2578 "progressToken": "7",
2579 "progress": 3,
2580 "total": 5,
2581 "message": "transferring",
2582 "cvm": { "type": "oversized-transfer", "frameType": "chunk", "data": "x" },
2583 });
2584 let stripped =
2585 NostrClientTransport::stripped_progress_notification(¶ms, &serde_json::json!(7))
2586 .expect("frame carries progress");
2587 let JsonRpcMessage::Notification(n) = stripped else {
2588 panic!("expected a notification");
2589 };
2590 assert_eq!(n.method, NOTIFICATIONS_PROGRESS_METHOD);
2591 let p = n.params.expect("params");
2592 assert_eq!(
2593 p["progressToken"],
2594 serde_json::json!(7),
2595 "token must be the restored original, not the wire string"
2596 );
2597 assert_eq!(p["progress"], serde_json::json!(3));
2598 assert_eq!(p["total"], serde_json::json!(5));
2599 assert_eq!(p["message"], serde_json::json!("transferring"));
2600 assert!(p.get("cvm").is_none(), "cvm payload must be stripped");
2601 }
2602
2603 #[test]
2604 fn stripped_progress_notification_requires_progress_and_omits_absent_fields() {
2605 let malformed = serde_json::json!({ "progressToken": "7", "cvm": {} });
2607 assert!(NostrClientTransport::stripped_progress_notification(
2608 &malformed,
2609 &serde_json::json!(7)
2610 )
2611 .is_none());
2612
2613 let minimal = serde_json::json!({ "progressToken": "7", "progress": 1 });
2615 let stripped =
2616 NostrClientTransport::stripped_progress_notification(&minimal, &serde_json::json!("7"))
2617 .expect("progress present");
2618 let JsonRpcMessage::Notification(n) = stripped else {
2619 panic!("expected a notification");
2620 };
2621 let p = n.params.expect("params");
2622 let keys = p.as_object().expect("object params");
2623 assert_eq!(keys.len(), 2, "only progressToken + progress: {p}");
2624 assert_eq!(p["progressToken"], serde_json::json!("7"));
2625 }
2626
2627 #[test]
2628 fn client_discovery_tags_sent_once() {
2629 let t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
2630 let first = t.get_pending_client_discovery_tags();
2631 assert!(!first.is_empty());
2632
2633 t.has_sent_discovery_tags.store(true, Ordering::Relaxed);
2634 let second = t.get_pending_client_discovery_tags();
2635 assert!(second.is_empty());
2636 }
2637
2638 fn make_event_with_tags(tag_parts: &[&[&str]]) -> Event {
2641 make_event_with_content_and_tags("{}", tag_parts)
2642 }
2643
2644 fn make_event_with_content_and_tags(content: &str, tag_parts: &[&[&str]]) -> Event {
2645 let keys = Keys::generate();
2646 let tags: Vec<Tag> = tag_parts.iter().map(|p| make_tag(p)).collect();
2647 let builder = EventBuilder::new(Kind::Custom(CTXVM_MESSAGES_KIND), content).tags(tags);
2648 let unsigned = builder.build(keys.public_key());
2649 unsigned.sign_with_keys(&keys).unwrap()
2650 }
2651
2652 fn initialize_result_content() -> String {
2654 serde_json::json!({
2655 "jsonrpc": "2.0",
2656 "id": 1,
2657 "result": {
2658 "protocolVersion": "2025-06-18",
2659 "capabilities": {},
2660 "serverInfo": { "name": "UpgradedServer", "version": "1.0.0" }
2661 }
2662 })
2663 .to_string()
2664 }
2665
2666 #[test]
2667 fn client_learn_server_discovery_sets_baseline() {
2668 let caps = Mutex::new(PeerCapabilities::default());
2669 let init = Mutex::new(None);
2670 let event = make_event_with_tags(&[&["support_encryption"], &["name", "TestServer"]]);
2671
2672 NostrClientTransport::learn_server_discovery(&caps, &init, &event);
2673
2674 let c = caps.lock().unwrap();
2675 assert!(c.supports_encryption);
2676 assert!(!c.supports_ephemeral_encryption);
2677
2678 let stored = init.lock().unwrap();
2679 assert!(stored.is_some());
2680 assert_eq!(stored.as_ref().unwrap().id, event.id);
2681 }
2682
2683 #[test]
2684 fn client_learn_server_discovery_or_assigns() {
2685 let caps = Mutex::new(PeerCapabilities::default());
2686 let init = Mutex::new(None);
2687
2688 let event1 = make_event_with_tags(&[&["support_encryption"]]);
2689 NostrClientTransport::learn_server_discovery(&caps, &init, &event1);
2690
2691 let event2 = make_event_with_tags(&[&["support_encryption_ephemeral"]]);
2693 NostrClientTransport::learn_server_discovery(&caps, &init, &event2);
2694
2695 let c = caps.lock().unwrap();
2696 assert!(c.supports_encryption, "must not downgrade");
2697 assert!(c.supports_ephemeral_encryption, "must learn new cap");
2698 }
2699
2700 #[test]
2701 fn client_baseline_not_replaced_on_later_events() {
2702 let caps = Mutex::new(PeerCapabilities::default());
2703 let init = Mutex::new(None);
2704
2705 let event1 = make_event_with_tags(&[&["support_encryption"], &["name", "First"]]);
2706 NostrClientTransport::learn_server_discovery(&caps, &init, &event1);
2707 let first_id = event1.id;
2708
2709 let event2 =
2710 make_event_with_tags(&[&["support_encryption_ephemeral"], &["name", "Second"]]);
2711 NostrClientTransport::learn_server_discovery(&caps, &init, &event2);
2712
2713 let stored = init.lock().unwrap();
2714 assert_eq!(
2715 stored.as_ref().unwrap().id,
2716 first_id,
2717 "baseline must not be replaced"
2718 );
2719 }
2720
2721 #[test]
2722 fn client_baseline_upgraded_to_initialize_result() {
2723 let caps = Mutex::new(PeerCapabilities::default());
2724 let init = Mutex::new(None);
2725
2726 let baseline = make_event_with_tags(&[&["support_encryption"]]);
2728 NostrClientTransport::learn_server_discovery(&caps, &init, &baseline);
2729 assert_eq!(init.lock().unwrap().as_ref().unwrap().id, baseline.id);
2730
2731 let init_event = make_event_with_content_and_tags(
2733 &initialize_result_content(),
2734 &[&["support_encryption"]],
2735 );
2736 NostrClientTransport::learn_server_discovery(&caps, &init, &init_event);
2737 assert_eq!(
2738 init.lock().unwrap().as_ref().unwrap().id,
2739 init_event.id,
2740 "baseline must upgrade to the initialize-result event"
2741 );
2742
2743 let later = make_event_with_tags(&[&["support_encryption_ephemeral"]]);
2745 NostrClientTransport::learn_server_discovery(&caps, &init, &later);
2746 assert_eq!(
2747 init.lock().unwrap().as_ref().unwrap().id,
2748 init_event.id,
2749 "baseline must not downgrade away from the initialize result"
2750 );
2751 }
2752}