Skip to main content

contextvm_sdk/transport/server/
mod.rs

1//! Server-side Nostr transport for ContextVM.
2//!
3//! Listens for incoming MCP requests from clients over Nostr, manages multi-client
4//! sessions, handles request/response correlation, and optionally publishes
5//! server announcements.
6
7pub 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/// Configuration for the server transport.
35#[derive(Debug, Clone)]
36#[non_exhaustive]
37pub struct NostrServerTransportConfig {
38    /// Relay URLs to connect to.
39    pub relay_urls: Vec<String>,
40    /// Encryption mode.
41    pub encryption_mode: EncryptionMode,
42    /// Gift-wrap kind selection policy (CEP-19).
43    pub gift_wrap_mode: GiftWrapMode,
44    /// Server information for announcements.
45    pub server_info: Option<ServerInfo>,
46    /// Whether this server publishes public announcements (CEP-6).
47    pub is_announced_server: bool,
48    /// Allowed client public keys (hex). Empty = allow all.
49    pub allowed_public_keys: Vec<String>,
50    /// Capabilities excluded from pubkey whitelisting.
51    pub excluded_capabilities: Vec<CapabilityExclusion>,
52    /// Maximum number of concurrent client sessions (LRU-bounded, default: 1000).
53    pub max_sessions: usize,
54    /// Session cleanup interval (default: 60s).
55    pub cleanup_interval: Duration,
56    /// Session timeout (default: 300s).
57    pub session_timeout: Duration,
58    /// Correlation-retention TTL for server-side event routes (default: 60s).
59    ///
60    /// Stale route entries older than this are swept from the correlation store.
61    /// This prevents leaks -- rmcp owns actual request timeout and cancellation.
62    /// Keep this value above your rmcp request timeout to avoid premature cleanup.
63    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
84/// Server-side Nostr transport — receives MCP requests and sends responses.
85pub struct NostrServerTransport {
86    /// Relay pool for publishing and subscribing.
87    base: BaseTransport,
88    /// Configuration for this server transport.
89    config: NostrServerTransportConfig,
90    /// Extra common discovery tags to include in server announcements and first responses.
91    extra_common_tags: Vec<Tag>,
92    /// Pricing tags to include in announcements and capability list responses.
93    pricing_tags: Vec<Tag>,
94    /// Client sessions.
95    sessions: SessionStore,
96    /// Reverse lookup: event_id → client route.
97    event_routes: ServerEventRouteStore,
98    /// CEP-19: Track the incoming gift-wrap kind per request for mirroring.
99    request_wrap_kinds: Arc<RwLock<HashMap<String, Option<u16>>>>,
100    /// Outer gift-wrap event IDs successfully decrypted and verified (inner `verify()`).
101    /// Duplicate outer ids are skipped before decrypt; ids are inserted only after success
102    /// so failed decrypt/verify can be retried on redelivery.
103    seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
104    /// Channel for incoming MCP messages (consumed by the MCP server).
105    message_tx: Option<tokio::sync::mpsc::UnboundedSender<IncomingRequest>>,
106    message_rx: Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>>,
107    /// Token used to cancel spawned tasks (event loop + cleanup) on close().
108    cancellation_token: CancellationToken,
109    /// Handles for spawned tasks (event loop + cleanup).
110    task_handles: Vec<tokio::task::JoinHandle<()>>,
111}
112
113impl NostrServerTransportConfig {
114    /// Set the encryption mode.
115    pub fn with_encryption_mode(mut self, mode: EncryptionMode) -> Self {
116        self.encryption_mode = mode;
117        self
118    }
119    /// Set the gift-wrap mode (CEP-19).
120    pub fn with_gift_wrap_mode(mut self, mode: GiftWrapMode) -> Self {
121        self.gift_wrap_mode = mode;
122        self
123    }
124    /// Set server information for announcements.
125    pub fn with_server_info(mut self, info: ServerInfo) -> Self {
126        self.server_info = Some(info);
127        self
128    }
129    /// Enable or disable public announcement publishing (CEP-6).
130    pub fn with_announced_server(mut self, announced: bool) -> Self {
131        self.is_announced_server = announced;
132        self
133    }
134    /// Set the allowed client public keys (hex). Empty = allow all.
135    pub fn with_allowed_public_keys(mut self, keys: Vec<String>) -> Self {
136        self.allowed_public_keys = keys;
137        self
138    }
139    /// Set capabilities excluded from pubkey whitelisting.
140    pub fn with_excluded_capabilities(mut self, caps: Vec<CapabilityExclusion>) -> Self {
141        self.excluded_capabilities = caps;
142        self
143    }
144    /// Set the maximum number of concurrent client sessions.
145    pub fn with_max_sessions(mut self, max: usize) -> Self {
146        self.max_sessions = max;
147        self
148    }
149    /// Set the relay URLs to connect to.
150    pub fn with_relay_urls(mut self, urls: Vec<String>) -> Self {
151        self.relay_urls = urls;
152        self
153    }
154    /// Set the session cleanup interval.
155    pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
156        self.cleanup_interval = interval;
157        self
158    }
159    /// Set the session timeout.
160    pub fn with_session_timeout(mut self, timeout: Duration) -> Self {
161        self.session_timeout = timeout;
162        self
163    }
164    /// Set the correlation-retention TTL for event routes.
165    pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
166        self.request_timeout = timeout;
167        self
168    }
169}
170
171/// An incoming MCP request with metadata for routing the response.
172#[derive(Debug)]
173#[non_exhaustive]
174pub struct IncomingRequest {
175    /// The parsed MCP message.
176    pub message: JsonRpcMessage,
177    /// The client's public key (hex).
178    pub client_pubkey: String,
179    /// The Nostr event ID (for response correlation).
180    pub event_id: String,
181    /// Whether the original message was encrypted.
182    pub is_encrypted: bool,
183}
184
185impl NostrServerTransport {
186    /// Create a new server transport.
187    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    /// Like [`new`](Self::new) but accepts an existing relay pool.
234    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    /// Start listening for incoming requests.
271    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        // Spawn event loop with cancellation support
312        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        // Spawn session cleanup with cancellation support
352        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                // Sweep stale route entries in active sessions (rmcp handles timeout errors).
390                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    /// Close the transport — cancels event loop and cleanup tasks, then disconnects.
423    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    /// Send a response back to the client that sent the original request.
436    pub async fn send_response(&self, event_id: &str, mut response: JsonRpcMessage) -> Result<()> {
437        // Consume the route up-front so only one concurrent responder can proceed
438        // for a given event_id.
439        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        // Restore original request ID
463        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        // CEP-35: include discovery tags on first response to this client
472        let discovery_tags = self.take_pending_server_discovery_tags(session);
473        drop(sessions_w);
474
475        // CEP-19: Look up the incoming wrap kind for mirroring
476        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            // Re-register route on publish failure so caller can retry.
532            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        // Clean up wrap-kind tracking
545        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            // Clean up progress token
550            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    /// Send a notification to a specific client.
569    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        // CEP-35: include discovery tags on first message to this client
583        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        // CEP-19: Look up mirrored wrap kind from correlated request
598        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    /// Broadcast a notification to all initialized clients.
629    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    /// Take the message receiver for consuming incoming requests.
652    pub fn take_message_receiver(
653        &mut self,
654    ) -> Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>> {
655        self.message_rx.take()
656    }
657
658    /// Sets extra discovery tags to include in announcements and first-response discovery replay.
659    pub fn set_announcement_extra_tags(&mut self, tags: Vec<Tag>) {
660        self.extra_common_tags = tags;
661    }
662
663    /// Sets pricing tags to include in announcement/list events and capability list responses.
664    pub fn set_announcement_pricing_tags(&mut self, tags: Vec<Tag>) {
665        self.pricing_tags = tags;
666    }
667
668    /// Publish server announcement (kind 11316).
669    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    /// Publish tools list (kind 11317).
724    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    /// Publish resources list (kind 11318).
735    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    /// Publish prompts list (kind 11320).
746    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    /// Publish resource templates list (kind 11319).
757    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    /// Delete server announcements (NIP-09 kind 5).
771    pub async fn delete_announcements(&self, reason: &str) -> Result<()> {
772        // We publish kind 5 events for each announcement kind
773        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    /// Publish tools list from rmcp typed tool descriptors.
787    #[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    /// Publish resources list from rmcp typed resource descriptors.
797    #[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    /// Publish prompts list from rmcp typed prompt descriptors.
810    #[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    /// Publish resource templates list from rmcp typed template descriptors.
823    #[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    // ── CEP-35 discovery tag helpers ──────────────────────────────
836
837    /// Build common discovery tags from server config.
838    ///
839    /// Includes server info tags (name, about, website, picture) and capability
840    /// tags (support_encryption, support_encryption_ephemeral) based on the
841    /// transport's encryption and gift-wrap mode.
842    fn get_common_tags(&self) -> Vec<Tag> {
843        let mut tags = Vec::new();
844
845        // Server info tags
846        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        // Capability tags
874        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    /// One-shot: returns common tags if not yet sent to this client, empty otherwise.
891    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    // ── Internal ────────────────────────────────────────────────
900
901    fn is_capability_excluded(
902        excluded: &[CapabilityExclusion],
903        method: &str,
904        name: Option<&str>,
905    ) -> bool {
906        // Always allow fundamental MCP methods
907        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, // method-only match
918                _ => 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                // CEP-19: Drop gift-wraps that violate the configured gift-wrap mode
972                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                    // Single-layer NIP-44 decrypt (matches JS/TS SDK)
1008                    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                            // The decrypted content is JSON of the inner signed event.
1022                            // Use the INNER event's ID for correlation — the client
1023                            // registers the inner event ID in its correlation store.
1024                            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                // Parse MCP message
1086                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                // Authorization check
1099                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                        // Send a JSON-RPC error back for Request messages so the
1122                        // client doesn't hang indefinitely (announced servers only).
1123                        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                                    // CEP-19: Inject common discovery tags on first response
1134                                    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                        } // if is_announced_server
1190
1191                        continue;
1192                    }
1193                }
1194
1195                // Session management
1196                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                // CEP-19: Mark ephemeral support if client used kind 21059
1215                if is_gift_wrap && outer_kind == EPHEMERAL_GIFT_WRAP_KIND {
1216                    session.supports_ephemeral_gift_wrap = true;
1217                }
1218
1219                // CEP-35: learn client capabilities from inner event tags
1220                let discovered = learn_peer_capabilities(&inner_tags);
1221                session.supports_encryption |= discovered.supports_encryption;
1222                session.supports_ephemeral_encryption |= discovered.supports_ephemeral_encryption;
1223                // Only learn oversized support if CEP-22 is enabled on this server
1224                // TODO: wire from config when CEP-22 lands
1225                let oversized_enabled = false;
1226                session.supports_oversized_transfer |=
1227                    oversized_enabled && discovered.supports_oversized_transfer;
1228
1229                // Track request for correlation
1230                if let JsonRpcMessage::Request(ref req) = mcp_msg {
1231                    let original_id = req.id.clone();
1232
1233                    // Extract progress token from _meta if present.
1234                    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                    // Duplicate into session fields (kept for backward compat).
1243                    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                    // CEP-19: Record the incoming wrap kind for response mirroring
1258                    {
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                // Handle initialized notification (re-acquire for write)
1279                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                // Forward to consumer
1289                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        // LruCache has no retain(); collect expired keys then pop each one.
1310        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    /// CEP-19: Choose outbound gift-wrap kind for responses.
1345    /// If `is_encrypted` is false, return None (send plaintext).
1346    /// Otherwise mirror the kind used by the client, falling back to the mode default.
1347    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    /// CEP-19: Choose outbound gift-wrap kind for notifications.
1368    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        // Mirror correlated request kind if available
1378        if let Some(kind) = correlated_wrap_kind {
1379            if mode.allows_kind(kind) {
1380                return Some(kind);
1381            }
1382        }
1383        // Fall back based on learned ephemeral support
1384        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    /// CEP-19: Append server capability discovery tags to the given tag vec.
1395    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    // ── Session management ──────────────────────────────────────
1432
1433    #[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        // Insert a session with an old activity time
1459        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        // With a long timeout, nothing should be cleaned
1476        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        // With zero timeout, it should be cleaned
1487        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    // ── Request ID correlation ──────────────────────────────────
1522
1523    #[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    // ── Authorization (is_capability_excluded) ──────────────────
1551
1552    #[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    // ── Encryption mode enforcement ─────────────────────────────
1640
1641    #[test]
1642    fn test_encryption_mode_default() {
1643        let config = NostrServerTransportConfig::default();
1644        assert_eq!(config.encryption_mode, EncryptionMode::Optional);
1645    }
1646
1647    // ── Config defaults ─────────────────────────────────────────
1648
1649    #[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    // ── CEP-19 helper logic ──────────────────────────────────────
1665
1666    #[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    // ── CEP-35 discovery tag helpers ────────────────────────────
1878
1879    #[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}