1pub mod correlation_store;
8pub mod session_store;
9
10pub use correlation_store::{RouteEntry, ServerEventRouteStore};
11pub use session_store::{SessionSnapshot, SessionStore};
12use tokio::sync::RwLock;
13
14use std::collections::HashMap;
15use std::num::NonZeroUsize;
16use std::sync::{Arc, Mutex};
17use std::time::Duration;
18
19use lru::LruCache;
20use nostr_sdk::prelude::*;
21use tokio_util::sync::CancellationToken;
22
23use crate::core::constants::*;
24use crate::core::error::{Error, Result};
25use crate::core::types::*;
26use crate::core::validation;
27use crate::encryption;
28use crate::relay::{RelayPool, RelayPoolTrait};
29use crate::transport::base::BaseTransport;
30use crate::transport::discovery_tags::learn_peer_capabilities;
31
32const LOG_TARGET: &str = "contextvm_sdk::transport::server";
33
34#[derive(Debug, Clone)]
36#[non_exhaustive]
37pub struct NostrServerTransportConfig {
38 pub relay_urls: Vec<String>,
40 pub encryption_mode: EncryptionMode,
42 pub gift_wrap_mode: GiftWrapMode,
44 pub server_info: Option<ServerInfo>,
46 pub is_announced_server: bool,
48 pub allowed_public_keys: Vec<String>,
50 pub excluded_capabilities: Vec<CapabilityExclusion>,
52 pub max_sessions: usize,
54 pub cleanup_interval: Duration,
56 pub session_timeout: Duration,
58 pub request_timeout: Duration,
64}
65
66impl Default for NostrServerTransportConfig {
67 fn default() -> Self {
68 Self {
69 relay_urls: vec!["wss://relay.damus.io".to_string()],
70 encryption_mode: EncryptionMode::Optional,
71 gift_wrap_mode: GiftWrapMode::Optional,
72 server_info: None,
73 is_announced_server: false,
74 allowed_public_keys: Vec::new(),
75 excluded_capabilities: Vec::new(),
76 max_sessions: session_store::DEFAULT_MAX_SESSIONS,
77 cleanup_interval: Duration::from_secs(60),
78 session_timeout: Duration::from_secs(300),
79 request_timeout: Duration::from_secs(60),
80 }
81 }
82}
83
84pub struct NostrServerTransport {
86 base: BaseTransport,
88 config: NostrServerTransportConfig,
90 extra_common_tags: Vec<Tag>,
92 pricing_tags: Vec<Tag>,
94 sessions: SessionStore,
96 event_routes: ServerEventRouteStore,
98 request_wrap_kinds: Arc<RwLock<HashMap<String, Option<u16>>>>,
100 seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
104 message_tx: Option<tokio::sync::mpsc::UnboundedSender<IncomingRequest>>,
106 message_rx: Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>>,
107 cancellation_token: CancellationToken,
109 task_handles: Vec<tokio::task::JoinHandle<()>>,
111}
112
113impl NostrServerTransportConfig {
114 pub fn with_encryption_mode(mut self, mode: EncryptionMode) -> Self {
116 self.encryption_mode = mode;
117 self
118 }
119 pub fn with_gift_wrap_mode(mut self, mode: GiftWrapMode) -> Self {
121 self.gift_wrap_mode = mode;
122 self
123 }
124 pub fn with_server_info(mut self, info: ServerInfo) -> Self {
126 self.server_info = Some(info);
127 self
128 }
129 pub fn with_announced_server(mut self, announced: bool) -> Self {
131 self.is_announced_server = announced;
132 self
133 }
134 pub fn with_allowed_public_keys(mut self, keys: Vec<String>) -> Self {
136 self.allowed_public_keys = keys;
137 self
138 }
139 pub fn with_excluded_capabilities(mut self, caps: Vec<CapabilityExclusion>) -> Self {
141 self.excluded_capabilities = caps;
142 self
143 }
144 pub fn with_max_sessions(mut self, max: usize) -> Self {
146 self.max_sessions = max;
147 self
148 }
149 pub fn with_relay_urls(mut self, urls: Vec<String>) -> Self {
151 self.relay_urls = urls;
152 self
153 }
154 pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
156 self.cleanup_interval = interval;
157 self
158 }
159 pub fn with_session_timeout(mut self, timeout: Duration) -> Self {
161 self.session_timeout = timeout;
162 self
163 }
164 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
166 self.request_timeout = timeout;
167 self
168 }
169}
170
171#[derive(Debug)]
173#[non_exhaustive]
174pub struct IncomingRequest {
175 pub message: JsonRpcMessage,
177 pub client_pubkey: String,
179 pub event_id: String,
181 pub is_encrypted: bool,
183}
184
185impl NostrServerTransport {
186 pub async fn new<T>(signer: T, config: NostrServerTransportConfig) -> Result<Self>
188 where
189 T: IntoNostrSigner,
190 {
191 let relay_pool: Arc<dyn RelayPoolTrait> =
192 Arc::new(RelayPool::new(signer).await.map_err(|error| {
193 tracing::error!(
194 target: LOG_TARGET,
195 error = %error,
196 "Failed to initialize relay pool for server transport"
197 );
198 error
199 })?);
200 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
201 let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new(
202 NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
203 )));
204
205 tracing::info!(
206 target: LOG_TARGET,
207 relay_count = config.relay_urls.len(),
208 announced = config.is_announced_server,
209 encryption_mode = ?config.encryption_mode,
210 gift_wrap_mode = ?config.gift_wrap_mode,
211 "Created server transport"
212 );
213 Ok(Self {
214 base: BaseTransport {
215 relay_pool,
216 encryption_mode: config.encryption_mode,
217 is_connected: false,
218 },
219 sessions: SessionStore::with_capacity(config.max_sessions),
220 config,
221 extra_common_tags: Vec::new(),
222 pricing_tags: Vec::new(),
223 event_routes: ServerEventRouteStore::new(),
224 request_wrap_kinds: Arc::new(RwLock::new(HashMap::new())),
225 seen_gift_wrap_ids,
226 message_tx: Some(tx),
227 message_rx: Some(rx),
228 cancellation_token: CancellationToken::new(),
229 task_handles: Vec::new(),
230 })
231 }
232
233 pub async fn with_relay_pool(
235 config: NostrServerTransportConfig,
236 relay_pool: Arc<dyn RelayPoolTrait>,
237 ) -> Result<Self> {
238 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
239 let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new(
240 NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
241 )));
242
243 tracing::info!(
244 target: LOG_TARGET,
245 relay_count = config.relay_urls.len(),
246 announced = config.is_announced_server,
247 encryption_mode = ?config.encryption_mode,
248 "Created server transport (with_relay_pool)"
249 );
250 Ok(Self {
251 base: BaseTransport {
252 relay_pool,
253 encryption_mode: config.encryption_mode,
254 is_connected: false,
255 },
256 sessions: SessionStore::with_capacity(config.max_sessions),
257 config,
258 extra_common_tags: Vec::new(),
259 pricing_tags: Vec::new(),
260 request_wrap_kinds: Arc::new(RwLock::new(HashMap::new())),
261 event_routes: ServerEventRouteStore::new(),
262 seen_gift_wrap_ids,
263 message_tx: Some(tx),
264 message_rx: Some(rx),
265 cancellation_token: CancellationToken::new(),
266 task_handles: Vec::new(),
267 })
268 }
269
270 pub async fn start(&mut self) -> Result<()> {
272 self.base
273 .connect(&self.config.relay_urls)
274 .await
275 .map_err(|error| {
276 tracing::error!(
277 target: LOG_TARGET,
278 error = %error,
279 "Failed to connect server transport to relays"
280 );
281 error
282 })?;
283
284 let pubkey = self.base.get_public_key().await.map_err(|error| {
285 tracing::error!(
286 target: LOG_TARGET,
287 error = %error,
288 "Failed to fetch server transport public key"
289 );
290 error
291 })?;
292 tracing::info!(
293 target: LOG_TARGET,
294 pubkey = %pubkey.to_hex(),
295 "Server transport started"
296 );
297
298 self.base
299 .subscribe_for_pubkey(&pubkey)
300 .await
301 .map_err(|error| {
302 tracing::error!(
303 target: LOG_TARGET,
304 error = %error,
305 pubkey = %pubkey.to_hex(),
306 "Failed to subscribe server transport for pubkey"
307 );
308 error
309 })?;
310
311 let relay_pool = Arc::clone(&self.base.relay_pool);
313 let sessions = self.sessions.clone();
314 let event_routes = self.event_routes.clone();
315 let request_wrap_kinds = self.request_wrap_kinds.clone();
316 let tx = self
317 .message_tx
318 .as_ref()
319 .expect("message_tx must exist before start()")
320 .clone();
321 let allowed = self.config.allowed_public_keys.clone();
322 let excluded = self.config.excluded_capabilities.clone();
323 let encryption_mode = self.config.encryption_mode;
324 let gift_wrap_mode = self.config.gift_wrap_mode;
325 let is_announced_server = self.config.is_announced_server;
326 let server_info = self.config.server_info.clone();
327 let extra_common_tags = self.extra_common_tags.clone();
328 let seen_gift_wrap_ids = self.seen_gift_wrap_ids.clone();
329 let event_loop_token = self.cancellation_token.child_token();
330
331 let event_loop_handle = tokio::spawn(async move {
332 Self::event_loop(
333 relay_pool,
334 sessions,
335 event_routes,
336 request_wrap_kinds,
337 tx,
338 allowed,
339 excluded,
340 encryption_mode,
341 gift_wrap_mode,
342 is_announced_server,
343 server_info,
344 extra_common_tags,
345 seen_gift_wrap_ids,
346 event_loop_token,
347 )
348 .await;
349 });
350
351 let sessions_cleanup = self.sessions.clone();
353 let event_routes_cleanup = self.event_routes.clone();
354 let request_wrap_kinds_cleanup = self.request_wrap_kinds.clone();
355 let cleanup_interval = self.config.cleanup_interval;
356 let session_timeout = self.config.session_timeout;
357 let request_timeout = self.config.request_timeout;
358 let cleanup_token = self.cancellation_token.child_token();
359
360 let cleanup_handle = tokio::spawn(async move {
361 let mut interval = tokio::time::interval(cleanup_interval);
362 loop {
363 tokio::select! {
364 _ = cleanup_token.cancelled() => {
365 tracing::info!(
366 target: LOG_TARGET,
367 "Server cleanup task cancelled"
368 );
369 break;
370 }
371 _ = interval.tick() => {
372 let cleaned = Self::cleanup_sessions(
373 &sessions_cleanup,
374 &event_routes_cleanup,
375 &request_wrap_kinds_cleanup,
376 session_timeout,
377 )
378 .await;
379 if cleaned > 0 {
380 tracing::info!(
381 target: LOG_TARGET,
382 cleaned_sessions = cleaned,
383 "Cleaned up inactive sessions"
384 );
385 }
386 }
387 }
388
389 let swept_event_ids = event_routes_cleanup
391 .sweep_stale_routes(request_timeout)
392 .await;
393 if !swept_event_ids.is_empty() {
394 let mut kinds_w = request_wrap_kinds_cleanup.write().await;
395 for event_id in &swept_event_ids {
396 kinds_w.remove(event_id);
397 }
398 drop(kinds_w);
399 tracing::warn!(
400 target: LOG_TARGET,
401 swept = swept_event_ids.len(),
402 timeout_secs = request_timeout.as_secs(),
403 "Swept stale event routes (rmcp handles timeout errors)"
404 );
405 }
406 }
407 });
408
409 self.task_handles.push(event_loop_handle);
410 self.task_handles.push(cleanup_handle);
411
412 tracing::info!(
413 target: LOG_TARGET,
414 relay_count = self.config.relay_urls.len(),
415 cleanup_interval_secs = self.config.cleanup_interval.as_secs(),
416 session_timeout_secs = self.config.session_timeout.as_secs(),
417 "Server transport loops spawned"
418 );
419 Ok(())
420 }
421
422 pub async fn close(&mut self) -> Result<()> {
424 self.cancellation_token.cancel();
425 for handle in self.task_handles.drain(..) {
426 let _ = handle.await;
427 }
428 self.message_tx.take();
429 self.base.disconnect().await?;
430 self.sessions.clear().await;
431 self.event_routes.clear().await;
432 Ok(())
433 }
434
435 pub async fn send_response(&self, event_id: &str, mut response: JsonRpcMessage) -> Result<()> {
437 let route = self.event_routes.pop(event_id).await.ok_or_else(|| {
440 tracing::error!(
441 target: LOG_TARGET,
442 event_id = %event_id,
443 "No client found for response correlation"
444 );
445 Error::Other(format!("No client found for event {event_id}"))
446 })?;
447
448 let client_pubkey_hex = route.client_pubkey;
449 let original_request_id = route.original_request_id;
450 let progress_token = route.progress_token;
451
452 let mut sessions_w = self.sessions.write().await;
453 let session = sessions_w.get_mut(&client_pubkey_hex).ok_or_else(|| {
454 tracing::error!(
455 target: LOG_TARGET,
456 client_pubkey = %client_pubkey_hex,
457 "No session for correlated client"
458 );
459 Error::Other(format!("No session for client {client_pubkey_hex}"))
460 })?;
461
462 match &mut response {
464 JsonRpcMessage::Response(r) => r.id = original_request_id.clone(),
465 JsonRpcMessage::ErrorResponse(r) => r.id = original_request_id.clone(),
466 _ => {}
467 }
468
469 let is_encrypted = session.is_encrypted;
470
471 let discovery_tags = self.take_pending_server_discovery_tags(session);
473 drop(sessions_w);
474
475 let mirrored_wrap_kind = self
477 .request_wrap_kinds
478 .read()
479 .await
480 .get(event_id)
481 .copied()
482 .flatten();
483
484 let client_pubkey = PublicKey::from_hex(&client_pubkey_hex).map_err(|error| {
485 tracing::error!(
486 target: LOG_TARGET,
487 error = %error,
488 client_pubkey = %client_pubkey_hex,
489 "Invalid client pubkey in session map"
490 );
491 Error::Other(error.to_string())
492 })?;
493
494 let event_id_parsed = EventId::from_hex(event_id).map_err(|error| {
495 tracing::error!(
496 target: LOG_TARGET,
497 error = %error,
498 event_id = %event_id,
499 "Invalid event id while sending response"
500 );
501 Error::Other(error.to_string())
502 })?;
503
504 let base_tags = BaseTransport::create_response_tags(&client_pubkey, &event_id_parsed);
505 let tags = BaseTransport::compose_outbound_tags(&base_tags, &discovery_tags, &[]);
506
507 if let Err(error) = self
508 .base
509 .send_mcp_message(
510 &response,
511 &client_pubkey,
512 CTXVM_MESSAGES_KIND,
513 tags,
514 Some(is_encrypted),
515 Self::select_outbound_gift_wrap_kind(
516 self.config.gift_wrap_mode,
517 is_encrypted,
518 mirrored_wrap_kind,
519 ),
520 )
521 .await
522 {
523 tracing::error!(
524 target: LOG_TARGET,
525 error = %error,
526 client_pubkey = %client_pubkey_hex,
527 event_id = %event_id,
528 "Failed to publish response message"
529 );
530
531 self.event_routes
533 .register(
534 event_id.to_string(),
535 client_pubkey_hex,
536 original_request_id,
537 progress_token,
538 )
539 .await;
540
541 return Err(error);
542 }
543
544 self.request_wrap_kinds.write().await.remove(event_id);
546
547 let mut sessions = self.sessions.write().await;
548 if let Some(session) = sessions.get_mut(&client_pubkey_hex) {
549 if let Some(token) = progress_token {
551 session.pending_requests.remove(&token);
552 }
553 session.event_to_progress_token.remove(event_id);
554 session.pending_requests.remove(event_id);
555 }
556 drop(sessions);
557
558 tracing::debug!(
559 target: LOG_TARGET,
560 client_pubkey = %client_pubkey_hex,
561 event_id = %event_id,
562 encrypted = is_encrypted,
563 "Sent server response and cleaned correlation state"
564 );
565 Ok(())
566 }
567
568 pub async fn send_notification(
570 &self,
571 client_pubkey_hex: &str,
572 notification: &JsonRpcMessage,
573 correlated_event_id: Option<&str>,
574 ) -> Result<()> {
575 let mut sessions = self.sessions.write().await;
576 let session = sessions
577 .get_mut(client_pubkey_hex)
578 .ok_or_else(|| Error::Other(format!("No session for {client_pubkey_hex}")))?;
579 let is_encrypted = session.is_encrypted;
580 let supports_ephemeral = session.supports_ephemeral_gift_wrap;
581
582 let discovery_tags = self.take_pending_server_discovery_tags(session);
584 drop(sessions);
585
586 let client_pubkey =
587 PublicKey::from_hex(client_pubkey_hex).map_err(|e| Error::Other(e.to_string()))?;
588
589 let mut base_tags = BaseTransport::create_recipient_tags(&client_pubkey);
590 if let Some(eid) = correlated_event_id {
591 let event_id = EventId::from_hex(eid).map_err(|e| Error::Other(e.to_string()))?;
592 base_tags.push(Tag::event(event_id));
593 }
594
595 let tags = BaseTransport::compose_outbound_tags(&base_tags, &discovery_tags, &[]);
596
597 let correlated_wrap_kind = if let Some(event_id) = correlated_event_id {
599 self.request_wrap_kinds
600 .read()
601 .await
602 .get(event_id)
603 .copied()
604 .flatten()
605 } else {
606 None
607 };
608
609 self.base
610 .send_mcp_message(
611 notification,
612 &client_pubkey,
613 CTXVM_MESSAGES_KIND,
614 tags,
615 Some(is_encrypted),
616 Self::select_outbound_notification_gift_wrap_kind(
617 self.config.gift_wrap_mode,
618 is_encrypted,
619 correlated_wrap_kind,
620 supports_ephemeral,
621 ),
622 )
623 .await?;
624
625 Ok(())
626 }
627
628 pub async fn broadcast_notification(&self, notification: &JsonRpcMessage) -> Result<()> {
630 let sessions = self.sessions.read().await;
631 let initialized: Vec<String> = sessions
632 .iter()
633 .filter(|(_, s)| s.is_initialized)
634 .map(|(k, _)| k.clone())
635 .collect();
636 drop(sessions);
637
638 for pubkey in initialized {
639 if let Err(error) = self.send_notification(&pubkey, notification, None).await {
640 tracing::error!(
641 target: LOG_TARGET,
642 error = %error,
643 client_pubkey = %pubkey,
644 "Failed to send notification"
645 );
646 }
647 }
648 Ok(())
649 }
650
651 pub fn take_message_receiver(
653 &mut self,
654 ) -> Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>> {
655 self.message_rx.take()
656 }
657
658 pub fn set_announcement_extra_tags(&mut self, tags: Vec<Tag>) {
660 self.extra_common_tags = tags;
661 }
662
663 pub fn set_announcement_pricing_tags(&mut self, tags: Vec<Tag>) {
665 self.pricing_tags = tags;
666 }
667
668 pub async fn announce(&self) -> Result<EventId> {
670 let info = self
671 .config
672 .server_info
673 .as_ref()
674 .ok_or_else(|| Error::Other("No server info configured".to_string()))?;
675
676 let content = serde_json::to_string(info)?;
677
678 let mut tags = Vec::new();
679 if let Some(ref name) = info.name {
680 tags.push(Tag::custom(
681 TagKind::Custom(tags::NAME.into()),
682 vec![name.clone()],
683 ));
684 }
685 if let Some(ref about) = info.about {
686 tags.push(Tag::custom(
687 TagKind::Custom(tags::ABOUT.into()),
688 vec![about.clone()],
689 ));
690 }
691 if let Some(ref website) = info.website {
692 tags.push(Tag::custom(
693 TagKind::Custom(tags::WEBSITE.into()),
694 vec![website.clone()],
695 ));
696 }
697 if let Some(ref picture) = info.picture {
698 tags.push(Tag::custom(
699 TagKind::Custom(tags::PICTURE.into()),
700 vec![picture.clone()],
701 ));
702 }
703 if self.config.encryption_mode != EncryptionMode::Disabled {
704 tags.push(Tag::custom(
705 TagKind::Custom(tags::SUPPORT_ENCRYPTION.into()),
706 Vec::<String>::new(),
707 ));
708 if self.config.gift_wrap_mode.supports_ephemeral() {
709 tags.push(Tag::custom(
710 TagKind::Custom(tags::SUPPORT_ENCRYPTION_EPHEMERAL.into()),
711 Vec::<String>::new(),
712 ));
713 }
714 }
715 tags.extend(self.extra_common_tags.iter().cloned());
716 tags.extend(self.pricing_tags.iter().cloned());
717
718 let builder = EventBuilder::new(Kind::Custom(SERVER_ANNOUNCEMENT_KIND), content).tags(tags);
719
720 self.base.relay_pool.publish(builder).await
721 }
722
723 pub async fn publish_tools(&self, tools: Vec<serde_json::Value>) -> Result<EventId> {
725 let content = serde_json::json!({ "tools": tools });
726 let builder = EventBuilder::new(
727 Kind::Custom(TOOLS_LIST_KIND),
728 serde_json::to_string(&content)?,
729 )
730 .tags(self.pricing_tags.iter().cloned());
731 self.base.relay_pool.publish(builder).await
732 }
733
734 pub async fn publish_resources(&self, resources: Vec<serde_json::Value>) -> Result<EventId> {
736 let content = serde_json::json!({ "resources": resources });
737 let builder = EventBuilder::new(
738 Kind::Custom(RESOURCES_LIST_KIND),
739 serde_json::to_string(&content)?,
740 )
741 .tags(self.pricing_tags.iter().cloned());
742 self.base.relay_pool.publish(builder).await
743 }
744
745 pub async fn publish_prompts(&self, prompts: Vec<serde_json::Value>) -> Result<EventId> {
747 let content = serde_json::json!({ "prompts": prompts });
748 let builder = EventBuilder::new(
749 Kind::Custom(PROMPTS_LIST_KIND),
750 serde_json::to_string(&content)?,
751 )
752 .tags(self.pricing_tags.iter().cloned());
753 self.base.relay_pool.publish(builder).await
754 }
755
756 pub async fn publish_resource_templates(
758 &self,
759 templates: Vec<serde_json::Value>,
760 ) -> Result<EventId> {
761 let content = serde_json::json!({ "resourceTemplates": templates });
762 let builder = EventBuilder::new(
763 Kind::Custom(RESOURCETEMPLATES_LIST_KIND),
764 serde_json::to_string(&content)?,
765 )
766 .tags(self.pricing_tags.iter().cloned());
767 self.base.relay_pool.publish(builder).await
768 }
769
770 pub async fn delete_announcements(&self, reason: &str) -> Result<()> {
772 let pubkey = self.base.get_public_key().await?;
774 let _pubkey_hex = pubkey.to_hex();
775
776 for kind in UNENCRYPTED_KINDS {
777 let builder = EventBuilder::new(Kind::Custom(5), reason).tag(Tag::custom(
778 TagKind::Custom("k".into()),
779 vec![kind.to_string()],
780 ));
781 self.base.relay_pool.publish(builder).await?;
782 }
783 Ok(())
784 }
785
786 #[cfg(feature = "rmcp")]
788 pub async fn publish_tools_typed(&self, tools: Vec<rmcp::model::Tool>) -> Result<EventId> {
789 let tools = tools
790 .into_iter()
791 .map(serde_json::to_value)
792 .collect::<std::result::Result<Vec<_>, _>>()?;
793 self.publish_tools(tools).await
794 }
795
796 #[cfg(feature = "rmcp")]
798 pub async fn publish_resources_typed(
799 &self,
800 resources: Vec<rmcp::model::Resource>,
801 ) -> Result<EventId> {
802 let resources = resources
803 .into_iter()
804 .map(serde_json::to_value)
805 .collect::<std::result::Result<Vec<_>, _>>()?;
806 self.publish_resources(resources).await
807 }
808
809 #[cfg(feature = "rmcp")]
811 pub async fn publish_prompts_typed(
812 &self,
813 prompts: Vec<rmcp::model::Prompt>,
814 ) -> Result<EventId> {
815 let prompts = prompts
816 .into_iter()
817 .map(serde_json::to_value)
818 .collect::<std::result::Result<Vec<_>, _>>()?;
819 self.publish_prompts(prompts).await
820 }
821
822 #[cfg(feature = "rmcp")]
824 pub async fn publish_resource_templates_typed(
825 &self,
826 templates: Vec<rmcp::model::ResourceTemplate>,
827 ) -> Result<EventId> {
828 let templates = templates
829 .into_iter()
830 .map(serde_json::to_value)
831 .collect::<std::result::Result<Vec<_>, _>>()?;
832 self.publish_resource_templates(templates).await
833 }
834
835 fn get_common_tags(&self) -> Vec<Tag> {
843 let mut tags = Vec::new();
844
845 if let Some(ref info) = self.config.server_info {
847 if let Some(ref name) = info.name {
848 tags.push(Tag::custom(
849 TagKind::Custom(tags::NAME.into()),
850 vec![name.clone()],
851 ));
852 }
853 if let Some(ref about) = info.about {
854 tags.push(Tag::custom(
855 TagKind::Custom(tags::ABOUT.into()),
856 vec![about.clone()],
857 ));
858 }
859 if let Some(ref website) = info.website {
860 tags.push(Tag::custom(
861 TagKind::Custom(tags::WEBSITE.into()),
862 vec![website.clone()],
863 ));
864 }
865 if let Some(ref picture) = info.picture {
866 tags.push(Tag::custom(
867 TagKind::Custom(tags::PICTURE.into()),
868 vec![picture.clone()],
869 ));
870 }
871 }
872
873 if self.config.encryption_mode != EncryptionMode::Disabled {
875 tags.push(Tag::custom(
876 TagKind::Custom(tags::SUPPORT_ENCRYPTION.into()),
877 Vec::<String>::new(),
878 ));
879 if self.config.gift_wrap_mode.supports_ephemeral() {
880 tags.push(Tag::custom(
881 TagKind::Custom(tags::SUPPORT_ENCRYPTION_EPHEMERAL.into()),
882 Vec::<String>::new(),
883 ));
884 }
885 }
886
887 tags
888 }
889
890 fn take_pending_server_discovery_tags(&self, session: &mut ClientSession) -> Vec<Tag> {
892 if session.has_sent_common_tags {
893 return vec![];
894 }
895 session.has_sent_common_tags = true;
896 self.get_common_tags()
897 }
898
899 fn is_capability_excluded(
902 excluded: &[CapabilityExclusion],
903 method: &str,
904 name: Option<&str>,
905 ) -> bool {
906 if method == "initialize" || method == "notifications/initialized" {
908 return true;
909 }
910
911 excluded.iter().any(|excl| {
912 if excl.method != method {
913 return false;
914 }
915 match (&excl.name, name) {
916 (Some(excl_name), Some(req_name)) => excl_name == req_name,
917 (None, _) => true, _ => false,
919 }
920 })
921 }
922
923 #[allow(clippy::too_many_arguments)]
924 async fn event_loop(
925 relay_pool: Arc<dyn RelayPoolTrait>,
926 sessions: SessionStore,
927 event_routes: ServerEventRouteStore,
928 request_wrap_kinds: Arc<RwLock<HashMap<String, Option<u16>>>>,
929 tx: tokio::sync::mpsc::UnboundedSender<IncomingRequest>,
930 allowed_pubkeys: Vec<String>,
931 excluded_capabilities: Vec<CapabilityExclusion>,
932 encryption_mode: EncryptionMode,
933 gift_wrap_mode: GiftWrapMode,
934 is_announced_server: bool,
935 server_info: Option<ServerInfo>,
936 extra_common_tags: Vec<Tag>,
937 seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
938 cancel: CancellationToken,
939 ) {
940 let mut notifications = relay_pool.notifications();
941
942 loop {
943 let notification = tokio::select! {
944 _ = cancel.cancelled() => {
945 tracing::info!(
946 target: LOG_TARGET,
947 "Server event loop cancelled"
948 );
949 break;
950 }
951 result = notifications.recv() => {
952 match result {
953 Ok(n) => n,
954 Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
955 tracing::warn!(
956 target: LOG_TARGET,
957 skipped = n,
958 "Relay broadcast lagged, skipping missed events"
959 );
960 continue;
961 }
962 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
963 }
964 }
965 };
966 if let RelayPoolNotification::Event { event, .. } = notification {
967 let is_gift_wrap = event.kind == Kind::Custom(GIFT_WRAP_KIND)
968 || event.kind == Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND);
969 let outer_kind: u16 = event.kind.as_u16();
970
971 if is_gift_wrap && !gift_wrap_mode.allows_kind(outer_kind) {
973 tracing::warn!(
974 target: LOG_TARGET,
975 event_id = %event.id.to_hex(),
976 event_kind = outer_kind,
977 configured_mode = ?gift_wrap_mode,
978 "Dropping gift-wrap because it violates gift_wrap_mode policy"
979 );
980 continue;
981 }
982
983 let (content, sender_pubkey, event_id, is_encrypted, inner_tags) = if is_gift_wrap {
984 if encryption_mode == EncryptionMode::Disabled {
985 tracing::warn!(
986 target: LOG_TARGET,
987 event_id = %event.id.to_hex(),
988 sender_pubkey = %event.pubkey.to_hex(),
989 "Received encrypted message but encryption is disabled"
990 );
991 continue;
992 }
993 {
994 let guard = match seen_gift_wrap_ids.lock() {
995 Ok(g) => g,
996 Err(poisoned) => poisoned.into_inner(),
997 };
998 if guard.contains(&event.id) {
999 tracing::debug!(
1000 target: LOG_TARGET,
1001 event_id = %event.id.to_hex(),
1002 "Skipping duplicate gift-wrap (outer id)"
1003 );
1004 continue;
1005 }
1006 }
1007 let signer = match relay_pool.signer().await {
1009 Ok(s) => s,
1010 Err(error) => {
1011 tracing::error!(
1012 target: LOG_TARGET,
1013 error = %error,
1014 "Failed to get signer"
1015 );
1016 continue;
1017 }
1018 };
1019 match encryption::decrypt_gift_wrap_single_layer(&signer, &event).await {
1020 Ok(decrypted_json) => {
1021 match serde_json::from_str::<Event>(&decrypted_json) {
1025 Ok(inner) => {
1026 if let Err(e) = inner.verify() {
1027 tracing::warn!(
1028 "Inner event signature verification failed: {e}"
1029 );
1030 continue;
1031 }
1032 {
1033 let mut guard = match seen_gift_wrap_ids.lock() {
1034 Ok(g) => g,
1035 Err(poisoned) => poisoned.into_inner(),
1036 };
1037 guard.put(event.id, ());
1038 }
1039 let inner_tags: Vec<Tag> = inner.tags.to_vec();
1040 (
1041 inner.content,
1042 inner.pubkey.to_hex(),
1043 inner.id.to_hex(),
1044 true,
1045 inner_tags,
1046 )
1047 }
1048 Err(error) => {
1049 tracing::error!(
1050 target: LOG_TARGET,
1051 error = %error,
1052 "Failed to parse inner event"
1053 );
1054 continue;
1055 }
1056 }
1057 }
1058 Err(error) => {
1059 tracing::error!(
1060 target: LOG_TARGET,
1061 error = %error,
1062 "Failed to decrypt"
1063 );
1064 continue;
1065 }
1066 }
1067 } else {
1068 if encryption_mode == EncryptionMode::Required {
1069 tracing::warn!(
1070 target: LOG_TARGET,
1071 sender_pubkey = %event.pubkey.to_hex(),
1072 "Received unencrypted message but encryption is required"
1073 );
1074 continue;
1075 }
1076 (
1077 event.content.clone(),
1078 event.pubkey.to_hex(),
1079 event.id.to_hex(),
1080 false,
1081 event.tags.to_vec(),
1082 )
1083 };
1084
1085 let mcp_msg = match validation::validate_and_parse(&content) {
1087 Some(msg) => msg,
1088 None => {
1089 tracing::warn!(
1090 target: LOG_TARGET,
1091 sender_pubkey = %sender_pubkey,
1092 "Invalid MCP message"
1093 );
1094 continue;
1095 }
1096 };
1097
1098 if !allowed_pubkeys.is_empty() {
1100 let method = mcp_msg.method().unwrap_or("");
1101 let name = match &mcp_msg {
1102 JsonRpcMessage::Request(r) => r
1103 .params
1104 .as_ref()
1105 .and_then(|p| p.get("name"))
1106 .and_then(|n| n.as_str()),
1107 _ => None,
1108 };
1109
1110 let is_excluded =
1111 Self::is_capability_excluded(&excluded_capabilities, method, name);
1112
1113 if !allowed_pubkeys.contains(&sender_pubkey) && !is_excluded {
1114 tracing::warn!(
1115 target: LOG_TARGET,
1116 sender_pubkey = %sender_pubkey,
1117 method = method,
1118 "Unauthorized request"
1119 );
1120
1121 if is_announced_server {
1124 if let JsonRpcMessage::Request(ref req) = mcp_msg {
1125 if let Ok(client_pk) = PublicKey::from_hex(&sender_pubkey) {
1126 let event_id_parsed = EventId::from_hex(&event_id)
1127 .unwrap_or(EventId::all_zeros());
1128 let mut tags = BaseTransport::create_response_tags(
1129 &client_pk,
1130 &event_id_parsed,
1131 );
1132
1133 let has_sent = sessions
1135 .get_session(&sender_pubkey)
1136 .await
1137 .is_some_and(|s| s.has_sent_common_tags);
1138 if !has_sent {
1139 Self::append_common_response_tags(
1140 &mut tags,
1141 server_info.as_ref(),
1142 &extra_common_tags,
1143 encryption_mode,
1144 gift_wrap_mode,
1145 );
1146 sessions.mark_common_tags_sent(&sender_pubkey).await;
1147 }
1148
1149 let error_response =
1150 JsonRpcMessage::ErrorResponse(JsonRpcErrorResponse {
1151 jsonrpc: "2.0".to_string(),
1152 id: req.id.clone(),
1153 error: JsonRpcError {
1154 code: -32000,
1155 message: "Unauthorized".to_string(),
1156 data: None,
1157 },
1158 });
1159
1160 let base = BaseTransport {
1161 relay_pool: Arc::clone(&relay_pool),
1162 encryption_mode,
1163 is_connected: true,
1164 };
1165 if let Err(e) = base
1166 .send_mcp_message(
1167 &error_response,
1168 &client_pk,
1169 CTXVM_MESSAGES_KIND,
1170 tags,
1171 Some(is_encrypted),
1172 Self::select_outbound_gift_wrap_kind(
1173 gift_wrap_mode,
1174 is_encrypted,
1175 if is_gift_wrap { Some(outer_kind) } else { None },
1176 ),
1177 )
1178 .await
1179 {
1180 tracing::error!(
1181 target: LOG_TARGET,
1182 error = %e,
1183 sender_pubkey = %sender_pubkey,
1184 "Failed to send unauthorized error response"
1185 );
1186 }
1187 }
1188 }
1189 } continue;
1192 }
1193 }
1194
1195 let on_evicted_cb = sessions.eviction_callback();
1197 let mut sessions_w = sessions.write().await;
1198 if !sessions_w.contains(&sender_pubkey) {
1199 let evicted =
1200 sessions_w.push(sender_pubkey.clone(), ClientSession::new(is_encrypted));
1201 SessionStore::handle_eviction(
1202 &sender_pubkey,
1203 evicted,
1204 &mut sessions_w,
1205 on_evicted_cb.as_ref(),
1206 &event_routes,
1207 )
1208 .await;
1209 }
1210 let session = sessions_w.get_mut(&sender_pubkey).unwrap();
1211 session.update_activity();
1212 session.is_encrypted = is_encrypted;
1213
1214 if is_gift_wrap && outer_kind == EPHEMERAL_GIFT_WRAP_KIND {
1216 session.supports_ephemeral_gift_wrap = true;
1217 }
1218
1219 let discovered = learn_peer_capabilities(&inner_tags);
1221 session.supports_encryption |= discovered.supports_encryption;
1222 session.supports_ephemeral_encryption |= discovered.supports_ephemeral_encryption;
1223 let oversized_enabled = false;
1226 session.supports_oversized_transfer |=
1227 oversized_enabled && discovered.supports_oversized_transfer;
1228
1229 if let JsonRpcMessage::Request(ref req) = mcp_msg {
1231 let original_id = req.id.clone();
1232
1233 let progress_token = req
1235 .params
1236 .as_ref()
1237 .and_then(|p| p.get("_meta"))
1238 .and_then(|m| m.get("progressToken"))
1239 .and_then(|t| t.as_str())
1240 .map(String::from);
1241
1242 session
1244 .pending_requests
1245 .insert(event_id.clone(), original_id.clone());
1246 if let Some(ref token) = progress_token {
1247 session
1248 .pending_requests
1249 .insert(token.clone(), serde_json::json!(event_id));
1250 session
1251 .event_to_progress_token
1252 .insert(event_id.clone(), token.clone());
1253 }
1254
1255 drop(sessions_w);
1256
1257 {
1259 let mut kinds_w = request_wrap_kinds.write().await;
1260 kinds_w.insert(
1261 event_id.clone(),
1262 if is_gift_wrap { Some(outer_kind) } else { None },
1263 );
1264 }
1265
1266 event_routes
1267 .register(
1268 event_id.clone(),
1269 sender_pubkey.clone(),
1270 original_id,
1271 progress_token,
1272 )
1273 .await;
1274 } else {
1275 drop(sessions_w);
1276 }
1277
1278 if let JsonRpcMessage::Notification(ref n) = mcp_msg {
1280 if n.method == "notifications/initialized" {
1281 let mut sessions_w2 = sessions.write().await;
1282 if let Some(session) = sessions_w2.get_mut(&sender_pubkey) {
1283 session.is_initialized = true;
1284 }
1285 }
1286 }
1287
1288 let _ = tx.send(IncomingRequest {
1290 message: mcp_msg,
1291 client_pubkey: sender_pubkey,
1292 event_id,
1293 is_encrypted,
1294 });
1295 }
1296 }
1297 }
1298
1299 async fn cleanup_sessions(
1300 sessions: &SessionStore,
1301 event_routes: &ServerEventRouteStore,
1302 request_wrap_kinds: &Arc<RwLock<HashMap<String, Option<u16>>>>,
1303 timeout: Duration,
1304 ) -> usize {
1305 let mut sessions_w = sessions.write().await;
1306 let mut cleaned = 0;
1307 let mut stale_event_ids = Vec::new();
1308
1309 let expired_keys: Vec<String> = sessions_w
1311 .iter()
1312 .filter(|(_, session)| session.last_activity.elapsed() > timeout)
1313 .map(|(k, _)| k.clone())
1314 .collect();
1315
1316 for key in &expired_keys {
1317 if let Some(session) = sessions_w.pop(key) {
1318 stale_event_ids.extend(session.pending_requests.keys().cloned());
1319 stale_event_ids.extend(session.event_to_progress_token.keys().cloned());
1320 tracing::debug!(
1321 target: LOG_TARGET,
1322 client_pubkey = %key,
1323 "Session expired"
1324 );
1325 cleaned += 1;
1326 }
1327 }
1328 drop(sessions_w);
1329
1330 {
1331 let mut kinds_w = request_wrap_kinds.write().await;
1332 for event_id in &stale_event_ids {
1333 kinds_w.remove(event_id);
1334 }
1335 }
1336
1337 for event_id in &stale_event_ids {
1338 event_routes.pop(event_id).await;
1339 }
1340
1341 cleaned
1342 }
1343
1344 fn select_outbound_gift_wrap_kind(
1348 mode: GiftWrapMode,
1349 is_encrypted: bool,
1350 mirrored_kind: Option<u16>,
1351 ) -> Option<u16> {
1352 if !is_encrypted {
1353 return None;
1354 }
1355 if let Some(kind) = mirrored_kind {
1356 if mode.allows_kind(kind) {
1357 return Some(kind);
1358 }
1359 }
1360 match mode {
1361 GiftWrapMode::Persistent => Some(GIFT_WRAP_KIND),
1362 GiftWrapMode::Ephemeral => Some(EPHEMERAL_GIFT_WRAP_KIND),
1363 GiftWrapMode::Optional => Some(GIFT_WRAP_KIND),
1364 }
1365 }
1366
1367 fn select_outbound_notification_gift_wrap_kind(
1369 mode: GiftWrapMode,
1370 is_encrypted: bool,
1371 correlated_wrap_kind: Option<u16>,
1372 client_supports_ephemeral: bool,
1373 ) -> Option<u16> {
1374 if !is_encrypted {
1375 return None;
1376 }
1377 if let Some(kind) = correlated_wrap_kind {
1379 if mode.allows_kind(kind) {
1380 return Some(kind);
1381 }
1382 }
1383 if client_supports_ephemeral && mode.supports_ephemeral() {
1385 return Some(EPHEMERAL_GIFT_WRAP_KIND);
1386 }
1387 match mode {
1388 GiftWrapMode::Persistent => Some(GIFT_WRAP_KIND),
1389 GiftWrapMode::Ephemeral => Some(EPHEMERAL_GIFT_WRAP_KIND),
1390 GiftWrapMode::Optional => Some(GIFT_WRAP_KIND),
1391 }
1392 }
1393
1394 fn append_common_response_tags(
1396 tags: &mut Vec<Tag>,
1397 server_info: Option<&ServerInfo>,
1398 extra_common_tags: &[Tag],
1399 encryption_mode: EncryptionMode,
1400 gift_wrap_mode: GiftWrapMode,
1401 ) {
1402 if encryption_mode != EncryptionMode::Disabled {
1403 tags.push(Tag::custom(
1404 TagKind::Custom(tags::SUPPORT_ENCRYPTION.into()),
1405 Vec::<String>::new(),
1406 ));
1407 if gift_wrap_mode.supports_ephemeral() {
1408 tags.push(Tag::custom(
1409 TagKind::Custom(tags::SUPPORT_ENCRYPTION_EPHEMERAL.into()),
1410 Vec::<String>::new(),
1411 ));
1412 }
1413 }
1414 if let Some(info) = server_info {
1415 if let Some(ref name) = info.name {
1416 tags.push(Tag::custom(
1417 TagKind::Custom(tags::NAME.into()),
1418 vec![name.clone()],
1419 ));
1420 }
1421 }
1422 tags.extend(extra_common_tags.iter().cloned());
1423 }
1424}
1425
1426#[cfg(test)]
1427mod tests {
1428 use super::*;
1429 use std::thread;
1430
1431 #[test]
1434 fn test_client_session_creation() {
1435 let session = ClientSession::new(true);
1436 assert!(!session.is_initialized);
1437 assert!(session.is_encrypted);
1438 assert!(!session.has_sent_common_tags);
1439 assert!(!session.supports_ephemeral_gift_wrap);
1440 assert!(session.pending_requests.is_empty());
1441 assert!(session.event_to_progress_token.is_empty());
1442 }
1443
1444 #[test]
1445 fn test_client_session_update_activity() {
1446 let mut session = ClientSession::new(false);
1447 let first = session.last_activity;
1448 thread::sleep(Duration::from_millis(10));
1449 session.update_activity();
1450 assert!(session.last_activity > first);
1451 }
1452
1453 #[tokio::test]
1454 async fn test_cleanup_sessions_removes_expired() {
1455 let sessions = SessionStore::new();
1456 let event_routes = ServerEventRouteStore::new();
1457
1458 let mut session = ClientSession::new(false);
1460 session
1461 .pending_requests
1462 .insert("evt1".to_string(), serde_json::json!(1));
1463 sessions.write().await.put("pubkey1".to_string(), session);
1464 event_routes
1465 .register(
1466 "evt1".to_string(),
1467 "pubkey1".to_string(),
1468 serde_json::json!(1),
1469 None,
1470 )
1471 .await;
1472
1473 let request_wrap_kinds = Arc::new(RwLock::new(HashMap::new()));
1474
1475 let cleaned = NostrServerTransport::cleanup_sessions(
1477 &sessions,
1478 &event_routes,
1479 &request_wrap_kinds,
1480 Duration::from_secs(300),
1481 )
1482 .await;
1483 assert_eq!(cleaned, 0);
1484 assert_eq!(sessions.session_count().await, 1);
1485
1486 thread::sleep(Duration::from_millis(5));
1488 let cleaned = NostrServerTransport::cleanup_sessions(
1489 &sessions,
1490 &event_routes,
1491 &request_wrap_kinds,
1492 Duration::from_millis(1),
1493 )
1494 .await;
1495 assert_eq!(cleaned, 1);
1496 assert_eq!(sessions.session_count().await, 0);
1497 assert!(event_routes.pop("evt1").await.is_none());
1498 }
1499
1500 #[tokio::test]
1501 async fn test_cleanup_preserves_active_sessions() {
1502 let sessions = SessionStore::new();
1503 let event_routes = ServerEventRouteStore::new();
1504 let request_wrap_kinds = Arc::new(RwLock::new(HashMap::new()));
1505
1506 sessions
1507 .get_or_create_session("active", false, &event_routes)
1508 .await;
1509
1510 let cleaned = NostrServerTransport::cleanup_sessions(
1511 &sessions,
1512 &event_routes,
1513 &request_wrap_kinds,
1514 Duration::from_secs(300),
1515 )
1516 .await;
1517 assert_eq!(cleaned, 0);
1518 assert_eq!(sessions.session_count().await, 1);
1519 }
1520
1521 #[test]
1524 fn test_pending_request_tracking() {
1525 let mut session = ClientSession::new(false);
1526 session
1527 .pending_requests
1528 .insert("event_abc".to_string(), serde_json::json!(42));
1529 assert_eq!(
1530 session.pending_requests.get("event_abc"),
1531 Some(&serde_json::json!(42))
1532 );
1533 }
1534
1535 #[test]
1536 fn test_progress_token_tracking() {
1537 let mut session = ClientSession::new(false);
1538 session
1539 .event_to_progress_token
1540 .insert("evt1".to_string(), "token1".to_string());
1541 session
1542 .pending_requests
1543 .insert("token1".to_string(), serde_json::json!("evt1"));
1544 assert_eq!(
1545 session.event_to_progress_token.get("evt1"),
1546 Some(&"token1".to_string())
1547 );
1548 }
1549
1550 #[test]
1553 fn test_initialize_always_excluded() {
1554 assert!(NostrServerTransport::is_capability_excluded(
1555 &[],
1556 "initialize",
1557 None
1558 ));
1559 assert!(NostrServerTransport::is_capability_excluded(
1560 &[],
1561 "notifications/initialized",
1562 None
1563 ));
1564 }
1565
1566 #[test]
1567 fn test_method_excluded_without_name() {
1568 let exclusions = vec![CapabilityExclusion {
1569 method: "tools/list".to_string(),
1570 name: None,
1571 }];
1572 assert!(NostrServerTransport::is_capability_excluded(
1573 &exclusions,
1574 "tools/list",
1575 None
1576 ));
1577 assert!(NostrServerTransport::is_capability_excluded(
1578 &exclusions,
1579 "tools/list",
1580 Some("anything")
1581 ));
1582 }
1583
1584 #[test]
1585 fn test_method_excluded_with_name() {
1586 let exclusions = vec![CapabilityExclusion {
1587 method: "tools/call".to_string(),
1588 name: Some("get_weather".to_string()),
1589 }];
1590 assert!(NostrServerTransport::is_capability_excluded(
1591 &exclusions,
1592 "tools/call",
1593 Some("get_weather")
1594 ));
1595 assert!(!NostrServerTransport::is_capability_excluded(
1596 &exclusions,
1597 "tools/call",
1598 Some("other_tool")
1599 ));
1600 assert!(!NostrServerTransport::is_capability_excluded(
1601 &exclusions,
1602 "tools/call",
1603 None
1604 ));
1605 }
1606
1607 #[test]
1608 fn test_non_excluded_method() {
1609 let exclusions = vec![CapabilityExclusion {
1610 method: "tools/list".to_string(),
1611 name: None,
1612 }];
1613 assert!(!NostrServerTransport::is_capability_excluded(
1614 &exclusions,
1615 "tools/call",
1616 None
1617 ));
1618 assert!(!NostrServerTransport::is_capability_excluded(
1619 &exclusions,
1620 "resources/list",
1621 None
1622 ));
1623 }
1624
1625 #[test]
1626 fn test_empty_exclusions_non_init_method() {
1627 assert!(!NostrServerTransport::is_capability_excluded(
1628 &[],
1629 "tools/list",
1630 None
1631 ));
1632 assert!(!NostrServerTransport::is_capability_excluded(
1633 &[],
1634 "tools/call",
1635 Some("x")
1636 ));
1637 }
1638
1639 #[test]
1642 fn test_encryption_mode_default() {
1643 let config = NostrServerTransportConfig::default();
1644 assert_eq!(config.encryption_mode, EncryptionMode::Optional);
1645 }
1646
1647 #[test]
1650 fn test_config_defaults() {
1651 let config = NostrServerTransportConfig::default();
1652 assert_eq!(config.relay_urls, vec!["wss://relay.damus.io".to_string()]);
1653 assert!(!config.is_announced_server);
1654 assert_eq!(config.gift_wrap_mode, GiftWrapMode::Optional);
1655 assert!(config.allowed_public_keys.is_empty());
1656 assert!(config.excluded_capabilities.is_empty());
1657 assert_eq!(config.max_sessions, 1000);
1658 assert_eq!(config.cleanup_interval, Duration::from_secs(60));
1659 assert_eq!(config.session_timeout, Duration::from_secs(300));
1660 assert_eq!(config.request_timeout, Duration::from_secs(60));
1661 assert!(config.server_info.is_none());
1662 }
1663
1664 #[test]
1667 fn test_select_outbound_gift_wrap_kind_plaintext() {
1668 assert_eq!(
1669 NostrServerTransport::select_outbound_gift_wrap_kind(
1670 GiftWrapMode::Optional,
1671 false,
1672 Some(GIFT_WRAP_KIND),
1673 ),
1674 None
1675 );
1676 }
1677
1678 #[test]
1679 fn test_select_outbound_gift_wrap_kind_mirrors_incoming() {
1680 assert_eq!(
1681 NostrServerTransport::select_outbound_gift_wrap_kind(
1682 GiftWrapMode::Optional,
1683 true,
1684 Some(EPHEMERAL_GIFT_WRAP_KIND),
1685 ),
1686 Some(EPHEMERAL_GIFT_WRAP_KIND)
1687 );
1688 }
1689
1690 #[test]
1691 fn test_select_outbound_gift_wrap_kind_persistent_mode_overrides_ephemeral() {
1692 assert_eq!(
1693 NostrServerTransport::select_outbound_gift_wrap_kind(
1694 GiftWrapMode::Persistent,
1695 true,
1696 Some(EPHEMERAL_GIFT_WRAP_KIND),
1697 ),
1698 Some(GIFT_WRAP_KIND)
1699 );
1700 }
1701
1702 #[test]
1703 fn test_append_common_response_tags_includes_encryption_when_optional() {
1704 let mut tags = Vec::new();
1705 NostrServerTransport::append_common_response_tags(
1706 &mut tags,
1707 None,
1708 &[],
1709 EncryptionMode::Optional,
1710 GiftWrapMode::Optional,
1711 );
1712 let kinds: Vec<String> = tags.iter().map(|t| format!("{:?}", t.kind())).collect();
1713 assert!(
1714 kinds.iter().any(|k| k.contains("support_encryption")),
1715 "should include support_encryption tag"
1716 );
1717 }
1718
1719 #[test]
1720 fn test_append_common_response_tags_no_encryption_when_disabled() {
1721 let mut tags = Vec::new();
1722 NostrServerTransport::append_common_response_tags(
1723 &mut tags,
1724 None,
1725 &[],
1726 EncryptionMode::Disabled,
1727 GiftWrapMode::Optional,
1728 );
1729 assert!(
1730 tags.is_empty(),
1731 "should not include encryption tags when encryption disabled"
1732 );
1733 }
1734
1735 #[test]
1736 fn test_select_outbound_notification_gift_wrap_kind_plaintext() {
1737 assert_eq!(
1738 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
1739 GiftWrapMode::Optional,
1740 false,
1741 Some(EPHEMERAL_GIFT_WRAP_KIND),
1742 true,
1743 ),
1744 None
1745 );
1746 }
1747
1748 #[test]
1749 fn test_select_outbound_notification_gift_wrap_kind_mirrors_correlated() {
1750 assert_eq!(
1751 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
1752 GiftWrapMode::Optional,
1753 true,
1754 Some(EPHEMERAL_GIFT_WRAP_KIND),
1755 false,
1756 ),
1757 Some(EPHEMERAL_GIFT_WRAP_KIND)
1758 );
1759 }
1760
1761 #[test]
1762 fn test_select_outbound_notification_gift_wrap_kind_falls_back_to_mode_if_correlated_not_allowed(
1763 ) {
1764 assert_eq!(
1765 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
1766 GiftWrapMode::Ephemeral,
1767 true,
1768 Some(GIFT_WRAP_KIND),
1769 false,
1770 ),
1771 Some(EPHEMERAL_GIFT_WRAP_KIND)
1772 );
1773 }
1774
1775 #[test]
1776 fn test_select_outbound_notification_gift_wrap_kind_uses_ephemeral_if_supported() {
1777 assert_eq!(
1778 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
1779 GiftWrapMode::Optional,
1780 true,
1781 None,
1782 true,
1783 ),
1784 Some(EPHEMERAL_GIFT_WRAP_KIND)
1785 );
1786 }
1787
1788 #[test]
1789 fn test_select_outbound_notification_gift_wrap_kind_uses_persistent_if_ephemeral_supported_but_mode_persistent(
1790 ) {
1791 assert_eq!(
1792 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
1793 GiftWrapMode::Persistent,
1794 true,
1795 None,
1796 true,
1797 ),
1798 Some(GIFT_WRAP_KIND)
1799 );
1800 }
1801
1802 #[test]
1803 fn test_select_outbound_notification_gift_wrap_kind_uses_default_mode_if_ephemeral_not_supported(
1804 ) {
1805 assert_eq!(
1806 NostrServerTransport::select_outbound_notification_gift_wrap_kind(
1807 GiftWrapMode::Optional,
1808 true,
1809 None,
1810 false,
1811 ),
1812 Some(GIFT_WRAP_KIND)
1813 );
1814 }
1815
1816 #[test]
1817 fn test_append_common_response_tags_includes_ephemeral_tag() {
1818 let mut tags = Vec::new();
1819 NostrServerTransport::append_common_response_tags(
1820 &mut tags,
1821 None,
1822 &[],
1823 EncryptionMode::Optional,
1824 GiftWrapMode::Optional,
1825 );
1826 let kinds: Vec<String> = tags.iter().map(|t| format!("{:?}", t.kind())).collect();
1827 assert!(
1828 kinds
1829 .iter()
1830 .any(|k| k.contains("support_encryption_ephemeral")),
1831 "should include support_encryption_ephemeral tag"
1832 );
1833 }
1834
1835 #[test]
1836 fn test_append_common_response_tags_includes_server_info() {
1837 let mut tags = Vec::new();
1838 let server_info = ServerInfo {
1839 name: Some("TestServer".to_string()),
1840 ..Default::default()
1841 };
1842 NostrServerTransport::append_common_response_tags(
1843 &mut tags,
1844 Some(&server_info),
1845 &[],
1846 EncryptionMode::Disabled,
1847 GiftWrapMode::Optional,
1848 );
1849 let tag_value = tags
1850 .iter()
1851 .find(|t| (*t).clone().to_vec().first().map(|s| s.as_str()) == Some("name"))
1852 .and_then(|t| t.clone().to_vec().get(1).cloned());
1853 assert_eq!(tag_value.as_deref(), Some("TestServer"));
1854 }
1855
1856 #[test]
1857 fn test_append_common_response_tags_extra_tags() {
1858 let mut tags = Vec::new();
1859 let extra_tags = vec![Tag::custom(
1860 TagKind::Custom("custom_tag".into()),
1861 vec!["value".to_string()],
1862 )];
1863 NostrServerTransport::append_common_response_tags(
1864 &mut tags,
1865 None,
1866 &extra_tags,
1867 EncryptionMode::Disabled,
1868 GiftWrapMode::Optional,
1869 );
1870 let tag_value = tags
1871 .iter()
1872 .find(|t| (*t).clone().to_vec().first().map(|s| s.as_str()) == Some("custom_tag"))
1873 .and_then(|t| t.clone().to_vec().get(1).cloned());
1874 assert_eq!(tag_value.as_deref(), Some("value"));
1875 }
1876
1877 #[test]
1880 fn test_cep35_client_session_new_fields_default_false() {
1881 let session = ClientSession::new(false);
1882 assert!(!session.has_sent_common_tags);
1883 assert!(!session.supports_encryption);
1884 assert!(!session.supports_ephemeral_encryption);
1885 assert!(!session.supports_oversized_transfer);
1886 }
1887
1888 #[test]
1889 fn test_cep35_capability_or_assign() {
1890 let mut session = ClientSession::new(false);
1891
1892 session.supports_encryption |= true;
1893 session.supports_ephemeral_encryption |= false;
1894
1895 session.supports_encryption |= false;
1896 session.supports_ephemeral_encryption |= true;
1897
1898 assert!(session.supports_encryption, "OR-assign must not downgrade");
1899 assert!(session.supports_ephemeral_encryption);
1900 assert!(!session.supports_oversized_transfer);
1901 }
1902
1903 #[test]
1904 fn test_config_gift_wrap_mode_default() {
1905 let config = NostrServerTransportConfig::default();
1906 assert_eq!(config.gift_wrap_mode, GiftWrapMode::Optional);
1907 }
1908}