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