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
32use crate::util::tracing_setup;
33
34const LOG_TARGET: &str = "contextvm_sdk::transport::server";
35
36/// Configuration for the server transport.
37#[non_exhaustive]
38pub struct NostrServerTransportConfig {
39    /// Relay URLs to connect to.
40    pub relay_urls: Vec<String>,
41    /// Encryption mode.
42    pub encryption_mode: EncryptionMode,
43    /// Gift-wrap kind selection policy (CEP-19).
44    pub gift_wrap_mode: GiftWrapMode,
45    /// Server information for announcements.
46    pub server_info: Option<ServerInfo>,
47    /// Whether this server publishes public announcements (CEP-6).
48    pub is_announced_server: bool,
49    /// Allowed client public keys (hex). Empty = allow all.
50    pub allowed_public_keys: Vec<String>,
51    /// Capabilities excluded from pubkey whitelisting.
52    pub excluded_capabilities: Vec<CapabilityExclusion>,
53    /// Maximum number of concurrent client sessions (LRU-bounded, default: 1000).
54    pub max_sessions: usize,
55    /// Session cleanup interval (default: 60s).
56    pub cleanup_interval: Duration,
57    /// Session timeout (default: 300s).
58    pub session_timeout: Duration,
59    /// Correlation-retention TTL for server-side event routes (default: 60s).
60    ///
61    /// Stale route entries older than this are swept from the correlation store.
62    /// This prevents leaks -- rmcp owns actual request timeout and cancellation.
63    /// Keep this value above your rmcp request timeout to avoid premature cleanup.
64    pub request_timeout: Duration,
65    /// Optional log file path. Logs always go to stdout and are also appended here when set.
66    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
88/// Server-side Nostr transport — receives MCP requests and sends responses.
89pub struct NostrServerTransport {
90    /// Relay pool for publishing and subscribing.
91    base: BaseTransport,
92    /// Configuration for this server transport.
93    config: NostrServerTransportConfig,
94    /// Extra common discovery tags to include in server announcements and first responses.
95    extra_common_tags: Vec<Tag>,
96    /// Pricing tags to include in announcements and capability list responses.
97    pricing_tags: Vec<Tag>,
98    /// Client sessions.
99    sessions: SessionStore,
100    /// Reverse lookup: event_id → client route.
101    event_routes: ServerEventRouteStore,
102    /// CEP-19: Track the incoming gift-wrap kind per request for mirroring.
103    request_wrap_kinds: Arc<RwLock<HashMap<String, Option<u16>>>>,
104    /// Outer gift-wrap event IDs successfully decrypted and verified (inner `verify()`).
105    /// Duplicate outer ids are skipped before decrypt; ids are inserted only after success
106    /// so failed decrypt/verify can be retried on redelivery.
107    seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
108    /// Channel for incoming MCP messages (consumed by the MCP server).
109    message_tx: Option<tokio::sync::mpsc::UnboundedSender<IncomingRequest>>,
110    message_rx: Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>>,
111    /// Token used to cancel spawned tasks (event loop + cleanup) on close().
112    cancellation_token: CancellationToken,
113    /// Handles for spawned tasks (event loop + cleanup).
114    task_handles: Vec<tokio::task::JoinHandle<()>>,
115}
116
117impl NostrServerTransportConfig {
118    /// Set the encryption mode.
119    pub fn with_encryption_mode(mut self, mode: EncryptionMode) -> Self {
120        self.encryption_mode = mode;
121        self
122    }
123    /// Set the gift-wrap mode (CEP-19).
124    pub fn with_gift_wrap_mode(mut self, mode: GiftWrapMode) -> Self {
125        self.gift_wrap_mode = mode;
126        self
127    }
128    /// Set server information for announcements.
129    pub fn with_server_info(mut self, info: ServerInfo) -> Self {
130        self.server_info = Some(info);
131        self
132    }
133    /// Enable or disable public announcement publishing (CEP-6).
134    pub fn with_announced_server(mut self, announced: bool) -> Self {
135        self.is_announced_server = announced;
136        self
137    }
138    /// Set the allowed client public keys (hex). Empty = allow all.
139    pub fn with_allowed_public_keys(mut self, keys: Vec<String>) -> Self {
140        self.allowed_public_keys = keys;
141        self
142    }
143    /// Set capabilities excluded from pubkey whitelisting.
144    pub fn with_excluded_capabilities(mut self, caps: Vec<CapabilityExclusion>) -> Self {
145        self.excluded_capabilities = caps;
146        self
147    }
148    /// Set the maximum number of concurrent client sessions.
149    pub fn with_max_sessions(mut self, max: usize) -> Self {
150        self.max_sessions = max;
151        self
152    }
153    /// Set the relay URLs to connect to.
154    pub fn with_relay_urls(mut self, urls: Vec<String>) -> Self {
155        self.relay_urls = urls;
156        self
157    }
158    /// Set the session cleanup interval.
159    pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
160        self.cleanup_interval = interval;
161        self
162    }
163    /// Set the session timeout.
164    pub fn with_session_timeout(mut self, timeout: Duration) -> Self {
165        self.session_timeout = timeout;
166        self
167    }
168    /// Set the correlation-retention TTL for event routes.
169    pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
170        self.request_timeout = timeout;
171        self
172    }
173    /// Set the log file path.
174    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/// An incoming MCP request with metadata for routing the response.
181#[derive(Debug)]
182#[non_exhaustive]
183pub struct IncomingRequest {
184    /// The parsed MCP message.
185    pub message: JsonRpcMessage,
186    /// The client's public key (hex).
187    pub client_pubkey: String,
188    /// The Nostr event ID (for response correlation).
189    pub event_id: String,
190    /// Whether the original message was encrypted.
191    pub is_encrypted: bool,
192}
193
194impl NostrServerTransport {
195    /// Create a new server transport.
196    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    /// Like [`new`](Self::new) but accepts an existing relay pool.
245    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    /// Start listening for incoming requests.
284    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        // Spawn event loop with cancellation support
325        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        // Spawn session cleanup with cancellation support
365        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                // Sweep stale route entries in active sessions (rmcp handles timeout errors).
403                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    /// Close the transport — cancels event loop and cleanup tasks, then disconnects.
436    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    /// Send a response back to the client that sent the original request.
449    pub async fn send_response(&self, event_id: &str, mut response: JsonRpcMessage) -> Result<()> {
450        // Consume the route up-front so only one concurrent responder can proceed
451        // for a given event_id.
452        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        // Restore original request ID
476        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        // CEP-35: include discovery tags on first response to this client
485        let discovery_tags = self.take_pending_server_discovery_tags(session);
486        drop(sessions_w);
487
488        // CEP-19: Look up the incoming wrap kind for mirroring
489        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            // Re-register route on publish failure so caller can retry.
545            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        // Clean up wrap-kind tracking
558        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            // Clean up progress token
563            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    /// Send a notification to a specific client.
582    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        // CEP-35: include discovery tags on first message to this client
596        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        // CEP-19: Look up mirrored wrap kind from correlated request
611        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    /// Broadcast a notification to all initialized clients.
642    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    /// Take the message receiver for consuming incoming requests.
665    pub fn take_message_receiver(
666        &mut self,
667    ) -> Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>> {
668        self.message_rx.take()
669    }
670
671    /// Sets extra discovery tags to include in announcements and first-response discovery replay.
672    pub fn set_announcement_extra_tags(&mut self, tags: Vec<Tag>) {
673        self.extra_common_tags = tags;
674    }
675
676    /// Sets pricing tags to include in announcement/list events and capability list responses.
677    pub fn set_announcement_pricing_tags(&mut self, tags: Vec<Tag>) {
678        self.pricing_tags = tags;
679    }
680
681    /// Publish server announcement (kind 11316).
682    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    /// Publish tools list (kind 11317).
737    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    /// Publish resources list (kind 11318).
748    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    /// Publish prompts list (kind 11320).
759    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    /// Publish resource templates list (kind 11319).
770    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    /// Delete server announcements (NIP-09 kind 5).
784    pub async fn delete_announcements(&self, reason: &str) -> Result<()> {
785        // We publish kind 5 events for each announcement kind
786        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    /// Publish tools list from rmcp typed tool descriptors.
800    #[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    /// Publish resources list from rmcp typed resource descriptors.
810    #[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    /// Publish prompts list from rmcp typed prompt descriptors.
823    #[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    /// Publish resource templates list from rmcp typed template descriptors.
836    #[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    // ── CEP-35 discovery tag helpers ──────────────────────────────
849
850    /// Build common discovery tags from server config.
851    ///
852    /// Includes server info tags (name, about, website, picture) and capability
853    /// tags (support_encryption, support_encryption_ephemeral) based on the
854    /// transport's encryption and gift-wrap mode.
855    fn get_common_tags(&self) -> Vec<Tag> {
856        let mut tags = Vec::new();
857
858        // Server info tags
859        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        // Capability tags
887        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    /// One-shot: returns common tags if not yet sent to this client, empty otherwise.
904    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    // ── Internal ────────────────────────────────────────────────
913
914    fn is_capability_excluded(
915        excluded: &[CapabilityExclusion],
916        method: &str,
917        name: Option<&str>,
918    ) -> bool {
919        // Always allow fundamental MCP methods
920        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, // method-only match
931                _ => 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                // CEP-19: Drop gift-wraps that violate the configured gift-wrap mode
985                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                    // Single-layer NIP-44 decrypt (matches JS/TS SDK)
1021                    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                            // The decrypted content is JSON of the inner signed event.
1035                            // Use the INNER event's ID for correlation — the client
1036                            // registers the inner event ID in its correlation store.
1037                            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                // Parse MCP message
1099                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                // Authorization check
1112                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                        // Send a JSON-RPC error back for Request messages so the
1135                        // client doesn't hang indefinitely (announced servers only).
1136                        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                                    // CEP-19: Inject common discovery tags on first response
1147                                    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                        } // if is_announced_server
1203
1204                        continue;
1205                    }
1206                }
1207
1208                // Session management
1209                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                // CEP-19: Mark ephemeral support if client used kind 21059
1228                if is_gift_wrap && outer_kind == EPHEMERAL_GIFT_WRAP_KIND {
1229                    session.supports_ephemeral_gift_wrap = true;
1230                }
1231
1232                // CEP-35: learn client capabilities from inner event tags
1233                let discovered = learn_peer_capabilities(&inner_tags);
1234                session.supports_encryption |= discovered.supports_encryption;
1235                session.supports_ephemeral_encryption |= discovered.supports_ephemeral_encryption;
1236                // Only learn oversized support if CEP-22 is enabled on this server
1237                // TODO: wire from config when CEP-22 lands
1238                let oversized_enabled = false;
1239                session.supports_oversized_transfer |=
1240                    oversized_enabled && discovered.supports_oversized_transfer;
1241
1242                // Track request for correlation
1243                if let JsonRpcMessage::Request(ref req) = mcp_msg {
1244                    let original_id = req.id.clone();
1245
1246                    // Extract progress token from _meta if present.
1247                    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                    // Duplicate into session fields (kept for backward compat).
1256                    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                    // CEP-19: Record the incoming wrap kind for response mirroring
1271                    {
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                // Handle initialized notification (re-acquire for write)
1292                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                // Forward to consumer
1302                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        // LruCache has no retain(); collect expired keys then pop each one.
1323        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    /// CEP-19: Choose outbound gift-wrap kind for responses.
1358    /// If `is_encrypted` is false, return None (send plaintext).
1359    /// Otherwise mirror the kind used by the client, falling back to the mode default.
1360    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    /// CEP-19: Choose outbound gift-wrap kind for notifications.
1381    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        // Mirror correlated request kind if available
1391        if let Some(kind) = correlated_wrap_kind {
1392            if mode.allows_kind(kind) {
1393                return Some(kind);
1394            }
1395        }
1396        // Fall back based on learned ephemeral support
1397        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    /// CEP-19: Append server capability discovery tags to the given tag vec.
1408    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    // ── Session management ──────────────────────────────────────
1445
1446    #[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        // Insert a session with an old activity time
1472        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        // With a long timeout, nothing should be cleaned
1489        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        // With zero timeout, it should be cleaned
1500        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    // ── Request ID correlation ──────────────────────────────────
1535
1536    #[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    // ── Authorization (is_capability_excluded) ──────────────────
1564
1565    #[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    // ── Encryption mode enforcement ─────────────────────────────
1653
1654    #[test]
1655    fn test_encryption_mode_default() {
1656        let config = NostrServerTransportConfig::default();
1657        assert_eq!(config.encryption_mode, EncryptionMode::Optional);
1658    }
1659
1660    // ── Config defaults ─────────────────────────────────────────
1661
1662    #[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    // ── CEP-19 helper logic ──────────────────────────────────────
1679
1680    #[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    // ── CEP-35 discovery tag helpers ────────────────────────────
1892
1893    #[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}