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(crate) mod announcement_manager;
8pub mod correlation_store;
9pub mod session_store;
10
11pub use correlation_store::{RouteEntry, ServerEventRouteStore};
12pub use session_store::{SessionSnapshot, SessionStore};
13use tokio::sync::Mutex as AsyncMutex;
14use tokio::sync::RwLock;
15
16use std::collections::HashMap;
17use std::num::NonZeroUsize;
18use std::sync::atomic::{AtomicU64, Ordering};
19use std::sync::{Arc, Mutex};
20use std::time::{Duration, Instant};
21
22use lru::LruCache;
23use nostr_sdk::prelude::*;
24use tokio_util::sync::CancellationToken;
25
26use crate::core::constants::*;
27use crate::core::error::{Error, Result};
28use crate::core::types::*;
29use crate::core::validation;
30use crate::encryption;
31use crate::relay::{RelayPool, RelayPoolTrait};
32use crate::transport::base::BaseTransport;
33use crate::transport::discovery_tags::learn_peer_capabilities;
34use crate::transport::open_stream::{
35    open_stream_frame_from_notification, FrameOutcome, KeepaliveAction, OnAbortHook, OnCloseHook,
36    OpenStreamConfig, OpenStreamFrame, OpenStreamReceiver, OpenStreamRegistryPolicy,
37    OpenStreamWriter, OpenStreamWriterOptions, PublishFrame,
38};
39use crate::transport::oversized_transfer::{
40    build_oversized_frames, progress_token_string, resolve_safe_chunk_size, OversizedFrame,
41    OversizedSenderOptions, OversizedTransferConfig, OversizedTransferReceiver, TransferPolicy,
42    ACCEPT_PROGRESS,
43};
44
45const LOG_TARGET: &str = "contextvm_sdk::transport::server";
46
47/// CEP-22: the `support_oversized_transfer` capability tags to advertise, or
48/// empty when oversized transfer is disabled.
49fn oversized_support_tags(config: &NostrServerTransportConfig) -> Vec<Tag> {
50    if config.oversized_transfer.enabled {
51        vec![Tag::custom(
52            TagKind::Custom(tags::SUPPORT_OVERSIZED_TRANSFER.into()),
53            Vec::<String>::new(),
54        )]
55    } else {
56        Vec::new()
57    }
58}
59
60/// CEP-41: the `support_open_stream` capability tag to advertise, or empty when
61/// open-stream is disabled. Mirrors [`oversized_support_tags`].
62fn open_stream_support_tags(config: &OpenStreamConfig) -> Vec<Tag> {
63    if config.enabled {
64        vec![Tag::custom(
65            TagKind::Custom(tags::SUPPORT_OPEN_STREAM.into()),
66            Vec::<String>::new(),
67        )]
68    } else {
69        Vec::new()
70    }
71}
72
73/// CEP-22 + CEP-41: the internal capability tags advertised on announcements and
74/// replayed on the first response to each client.
75fn internal_common_capability_tags(config: &NostrServerTransportConfig) -> Vec<Tag> {
76    let mut tags = oversized_support_tags(config);
77    tags.extend(open_stream_support_tags(&config.open_stream));
78    tags
79}
80
81/// CEP-22: build the empty per-peer reassembly store, bounded to `max_sessions`
82/// peers (one [`OversizedTransferReceiver`] per client pubkey, inserted lazily by
83/// the inbound event loop).
84fn new_oversized_receiver_store(
85    max_sessions: usize,
86) -> Arc<RwLock<LruCache<String, OversizedTransferReceiver>>> {
87    Arc::new(RwLock::new(LruCache::new(
88        NonZeroUsize::new(max_sessions).unwrap_or(NonZeroUsize::new(1).unwrap()),
89    )))
90}
91
92/// CEP-41: build the empty per-peer open-stream reader store, bounded to
93/// `max_sessions` peers. Mirrors [`new_oversized_receiver_store`]; one
94/// [`OpenStreamReceiver`] per client pubkey is inserted lazily for inbound
95/// (client→server) streams.
96///
97/// Uses a [`tokio::sync::Mutex`] rather than an `RwLock`: the registry's
98/// `FnOnce` lifecycle hooks are `Send` but not `Sync`, so a shared-read lock
99/// could not be made `Sync`; the store is write-only anyway (`process_frame`
100/// needs `&mut`), so exclusive access loses nothing.
101fn new_open_stream_receiver_store(
102    max_sessions: usize,
103) -> Arc<AsyncMutex<LruCache<String, OpenStreamReceiver>>> {
104    Arc::new(AsyncMutex::new(LruCache::new(
105        NonZeroUsize::new(max_sessions).unwrap_or(NonZeroUsize::new(1).unwrap()),
106    )))
107}
108
109/// CEP-41: response-routing fields captured at writer creation, while the
110/// request's event route is fresh.
111///
112/// The deferred final response is delivered from this snapshot (via
113/// [`NostrServerTransport::send_open_stream_deferred_response`]) rather than from
114/// `event_routes`, so a stream that outlives `request_timeout` — after which the
115/// route is swept — still delivers its response. `mirrored_wrap_kind` mirrors
116/// the inbound gift-wrap kind for CEP-19, exactly as `send_response` does.
117#[derive(Clone)]
118struct RouteSnapshot {
119    client_pubkey: PublicKey,
120    original_request_id: serde_json::Value,
121    is_encrypted: bool,
122    mirrored_wrap_kind: Option<u16>,
123}
124
125/// CEP-41: the per-stream coordination slot for a server→client writer, keyed by
126/// request `event_id` in [`ServerOpenStreamState::slots`].
127///
128/// A single mutex over the whole map serializes the two writers of the deferred
129/// final response — `send_response` (the worker task) and the writer's
130/// close/abort hook (the tool task) — against the [`terminated`](Self::terminated)
131/// flag, so the response is never both stashed *and* dropped under a race.
132struct OpenStreamSlot {
133    writer: OpenStreamWriter,
134    snapshot: RouteSnapshot,
135    /// The final response, stashed by `send_response` when it arrives before the
136    /// stream closes (ordering A).
137    pending_response: Option<JsonRpcMessage>,
138    /// Set by the writer's close/abort hook once the stream is terminal. When
139    /// `send_response` arrives after this (ordering B), it delivers immediately.
140    terminated: bool,
141}
142
143/// CEP-41: the open-stream runtime state shared between the server transport and
144/// its spawned event loop. Bundled so the event-loop signature stays manageable.
145#[derive(Clone)]
146struct ServerOpenStreamState {
147    /// Master gate (`config.open_stream.enabled`).
148    enabled: bool,
149    /// Reader admission/buffering/keepalive policy projected from config.
150    policy: OpenStreamRegistryPolicy,
151    /// Per-peer reader engines for inbound (client→server) streams.
152    receiver: Arc<AsyncMutex<LruCache<String, OpenStreamReceiver>>>,
153    /// Per-stream writer + deferred-response slots, keyed by `event_id`.
154    slots: Arc<Mutex<HashMap<String, OpenStreamSlot>>>,
155    /// `progress_token → event_id`, so inbound control frames and the keepalive
156    /// sweep resolve the writer/route without consulting the route store.
157    token_to_event: Arc<Mutex<HashMap<String, String>>>,
158    /// Monotonic `progress` source for server-*as-reader* control frames
159    /// (`accept`/`pong`/`ping` on inbound client→server streams, where no writer
160    /// owns the counter). Per-token monotonicity holds even though it is shared.
161    control_progress: Arc<AtomicU64>,
162}
163
164impl ServerOpenStreamState {
165    fn new(config: &OpenStreamConfig, max_sessions: usize) -> Self {
166        Self {
167            enabled: config.enabled,
168            policy: config.into(),
169            receiver: new_open_stream_receiver_store(max_sessions),
170            slots: Arc::new(Mutex::new(HashMap::new())),
171            token_to_event: Arc::new(Mutex::new(HashMap::new())),
172            control_progress: Arc::new(AtomicU64::new(0)),
173        }
174    }
175
176    /// Next monotonic control-frame `progress` (1, 2, 3, …) for the reader path.
177    fn next_control_progress(&self) -> u64 {
178        self.control_progress.fetch_add(1, Ordering::SeqCst) + 1
179    }
180
181    /// Lock-poison-tolerant access to the slots map.
182    fn lock_slots(&self) -> std::sync::MutexGuard<'_, HashMap<String, OpenStreamSlot>> {
183        match self.slots.lock() {
184            Ok(g) => g,
185            Err(p) => p.into_inner(),
186        }
187    }
188
189    fn lock_token_index(&self) -> std::sync::MutexGuard<'_, HashMap<String, String>> {
190        match self.token_to_event.lock() {
191            Ok(g) => g,
192            Err(p) => p.into_inner(),
193        }
194    }
195
196    /// Clone the active writer for `event_id`, if any (for inbound ping/abort
197    /// routing and the worker's extensions injection).
198    fn writer_for(&self, event_id: &str) -> Option<OpenStreamWriter> {
199        self.lock_slots().get(event_id).map(|s| s.writer.clone())
200    }
201
202    /// Resolve `progress_token → event_id`.
203    fn event_id_for_token(&self, token: &str) -> Option<String> {
204        self.lock_token_index().get(token).cloned()
205    }
206}
207
208/// CEP-41: the outcome of the response-deferral decision in `send_response`.
209enum OpenStreamDeferral {
210    /// The response was stashed; the writer's close/abort hook will flush it.
211    Deferred,
212    /// The stream is already terminal — deliver this response now from the snapshot.
213    SendNow {
214        snapshot: RouteSnapshot,
215        response: JsonRpcMessage,
216    },
217    /// No active stream for this event — send the response through the normal path.
218    Passthrough(JsonRpcMessage),
219}
220
221/// Configuration for the server transport.
222#[derive(Debug, Clone)]
223#[non_exhaustive]
224pub struct NostrServerTransportConfig {
225    /// Relay URLs to connect to.
226    pub relay_urls: Vec<String>,
227    /// Encryption mode.
228    pub encryption_mode: EncryptionMode,
229    /// Gift-wrap kind selection policy (CEP-19).
230    pub gift_wrap_mode: GiftWrapMode,
231    /// Server information for announcements.
232    pub server_info: Option<ServerInfo>,
233    /// Whether this server publishes public announcements (CEP-6).
234    pub is_announced_server: bool,
235    /// Allowed client public keys (hex). Empty = allow all.
236    pub allowed_public_keys: Vec<String>,
237    /// Capabilities excluded from pubkey whitelisting.
238    pub excluded_capabilities: Vec<CapabilityExclusion>,
239    /// Maximum number of concurrent client sessions (LRU-bounded, default: 1000).
240    pub max_sessions: usize,
241    /// Session cleanup interval (default: 60s).
242    pub cleanup_interval: Duration,
243    /// Session timeout (default: 300s).
244    pub session_timeout: Duration,
245    /// Correlation-retention TTL for server-side event routes (default: 60s).
246    ///
247    /// Stale route entries older than this are swept from the correlation store.
248    /// This prevents leaks -- rmcp owns actual request timeout and cancellation.
249    /// Keep this value above your rmcp request timeout to avoid premature cleanup.
250    pub request_timeout: Duration,
251    /// Explicit relay URLs to advertise in kind 10002 (NIP-65 relay list).
252    ///
253    /// Falls back to the transport's `relay_urls` when omitted.
254    pub relay_list_urls: Option<Vec<String>>,
255    /// Additional publication targets for discoverability events.
256    ///
257    /// Merged with `relay_list_urls` when computing where to send events.
258    /// Defaults to [`DEFAULT_BOOTSTRAP_RELAY_URLS`] when omitted.
259    pub bootstrap_relay_urls: Option<Vec<String>>,
260    /// Whether to publish a relay list event (kind 10002). Default: `true`.
261    pub publish_relay_list: bool,
262    /// Optional NIP-01 profile metadata (kind 0) to publish at startup.
263    pub profile_metadata: Option<ProfileMetadata>,
264    /// CEP-22 oversized payload transfer configuration. Enabled by default.
265    pub oversized_transfer: OversizedTransferConfig,
266    /// CEP-41 open-stream configuration. Disabled by default (opt-in).
267    ///
268    /// When enabled, drives capability advertisement/learning, server→client
269    /// writers, response deferral, and the keepalive sweep. Opt in with
270    /// `OpenStreamConfig::enabled()` / `with_enabled(true)`.
271    pub open_stream: OpenStreamConfig,
272}
273
274impl Default for NostrServerTransportConfig {
275    fn default() -> Self {
276        Self {
277            relay_urls: vec!["wss://relay.damus.io".to_string()],
278            encryption_mode: EncryptionMode::Optional,
279            gift_wrap_mode: GiftWrapMode::Optional,
280            server_info: None,
281            is_announced_server: false,
282            allowed_public_keys: Vec::new(),
283            excluded_capabilities: Vec::new(),
284            max_sessions: session_store::DEFAULT_MAX_SESSIONS,
285            cleanup_interval: Duration::from_secs(60),
286            session_timeout: Duration::from_secs(300),
287            request_timeout: Duration::from_secs(60),
288            relay_list_urls: None,
289            bootstrap_relay_urls: None,
290            publish_relay_list: true,
291            profile_metadata: None,
292            oversized_transfer: OversizedTransferConfig::default(),
293            open_stream: OpenStreamConfig::default(),
294        }
295    }
296}
297
298/// Server-side Nostr transport — receives MCP requests and sends responses.
299pub struct NostrServerTransport {
300    /// Relay pool for publishing and subscribing.
301    base: BaseTransport,
302    /// Configuration for this server transport.
303    config: NostrServerTransportConfig,
304    /// Manages tag composition and publishing for CEP-6 announcements and CEP-35 discovery.
305    announcement_manager: announcement_manager::AnnouncementManager,
306    /// Client sessions.
307    sessions: SessionStore,
308    /// Reverse lookup: event_id → client route.
309    event_routes: ServerEventRouteStore,
310    /// CEP-19: Track the incoming gift-wrap kind per request for mirroring.
311    request_wrap_kinds: Arc<RwLock<HashMap<String, Option<u16>>>>,
312    /// Outer gift-wrap event IDs successfully decrypted and verified (inner `verify()`).
313    /// Duplicate outer ids are skipped before decrypt; ids are inserted only after success
314    /// so failed decrypt/verify can be retried on redelivery.
315    seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
316    /// CEP-22: per-peer reassembly engines for inbound oversized transfers, keyed
317    /// by client pubkey (hex) and bounded to `max_sessions` peers. Each receiver
318    /// enforces the configured per-peer admission policy. Populated by the inbound
319    /// event loop; cleared on [`close`](Self::close).
320    oversized_receiver: Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
321    /// CEP-41: open-stream runtime state (writers, deferred responses, per-peer
322    /// reader engines, `progress_token → event_id` index). Inert when
323    /// `open_stream.enabled` is `false`.
324    open_stream: ServerOpenStreamState,
325    /// Channel for incoming MCP messages (consumed by the MCP server).
326    message_tx: Option<tokio::sync::mpsc::UnboundedSender<IncomingRequest>>,
327    message_rx: Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>>,
328    /// Token used to cancel spawned tasks (event loop + cleanup) on close().
329    cancellation_token: CancellationToken,
330    /// Handles for spawned tasks (event loop + cleanup).
331    task_handles: Vec<tokio::task::JoinHandle<()>>,
332}
333
334impl NostrServerTransportConfig {
335    /// Set the encryption mode.
336    pub fn with_encryption_mode(mut self, mode: EncryptionMode) -> Self {
337        self.encryption_mode = mode;
338        self
339    }
340    /// Set the gift-wrap mode (CEP-19).
341    pub fn with_gift_wrap_mode(mut self, mode: GiftWrapMode) -> Self {
342        self.gift_wrap_mode = mode;
343        self
344    }
345    /// Set server information for announcements.
346    pub fn with_server_info(mut self, info: ServerInfo) -> Self {
347        self.server_info = Some(info);
348        self
349    }
350    /// Enable or disable public announcement publishing (CEP-6).
351    pub fn with_announced_server(mut self, announced: bool) -> Self {
352        self.is_announced_server = announced;
353        self
354    }
355    /// Set the allowed client public keys (hex). Empty = allow all.
356    pub fn with_allowed_public_keys(mut self, keys: Vec<String>) -> Self {
357        self.allowed_public_keys = keys;
358        self
359    }
360    /// Set capabilities excluded from pubkey whitelisting.
361    pub fn with_excluded_capabilities(mut self, caps: Vec<CapabilityExclusion>) -> Self {
362        self.excluded_capabilities = caps;
363        self
364    }
365    /// Set the maximum number of concurrent client sessions.
366    pub fn with_max_sessions(mut self, max: usize) -> Self {
367        self.max_sessions = max;
368        self
369    }
370    /// Set the relay URLs to connect to.
371    pub fn with_relay_urls(mut self, urls: Vec<String>) -> Self {
372        self.relay_urls = urls;
373        self
374    }
375    /// Set the session cleanup interval.
376    pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
377        self.cleanup_interval = interval;
378        self
379    }
380    /// Set the session timeout.
381    pub fn with_session_timeout(mut self, timeout: Duration) -> Self {
382        self.session_timeout = timeout;
383        self
384    }
385    /// Set the correlation-retention TTL for event routes.
386    pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
387        self.request_timeout = timeout;
388        self
389    }
390    /// Set explicit relay URLs to advertise in the relay list event (kind 10002).
391    pub fn with_relay_list_urls(mut self, urls: Vec<String>) -> Self {
392        self.relay_list_urls = Some(urls);
393        self
394    }
395    /// Set additional bootstrap relay URLs for discoverability event publication.
396    pub fn with_bootstrap_relay_urls(mut self, urls: Vec<String>) -> Self {
397        self.bootstrap_relay_urls = Some(urls);
398        self
399    }
400    /// Enable or disable relay list publication (kind 10002).
401    pub fn with_publish_relay_list(mut self, publish: bool) -> Self {
402        self.publish_relay_list = publish;
403        self
404    }
405    /// Set NIP-01 profile metadata (kind 0) for publication at startup.
406    pub fn with_profile_metadata(mut self, metadata: ProfileMetadata) -> Self {
407        self.profile_metadata = Some(metadata);
408        self
409    }
410    /// Set the full CEP-22 oversized payload transfer configuration.
411    pub fn with_oversized_transfer(mut self, config: OversizedTransferConfig) -> Self {
412        self.oversized_transfer = config;
413        self
414    }
415    /// Enable or disable CEP-22 oversized payload transfer, leaving other knobs at default.
416    pub fn with_oversized_enabled(mut self, enabled: bool) -> Self {
417        self.oversized_transfer.enabled = enabled;
418        self
419    }
420    /// Set the full CEP-41 open-stream configuration (disabled by default; opt in
421    /// with `OpenStreamConfig::enabled()`).
422    pub fn with_open_stream(mut self, config: OpenStreamConfig) -> Self {
423        self.open_stream = config;
424        self
425    }
426}
427
428/// An incoming MCP request with metadata for routing the response.
429#[derive(Debug)]
430#[non_exhaustive]
431pub struct IncomingRequest {
432    /// The parsed MCP message.
433    pub message: JsonRpcMessage,
434    /// The client's public key (hex).
435    pub client_pubkey: String,
436    /// The Nostr event ID (for response correlation).
437    pub event_id: String,
438    /// Whether the original message was encrypted.
439    pub is_encrypted: bool,
440}
441
442impl NostrServerTransport {
443    /// Create a new server transport.
444    pub async fn new<T>(signer: T, config: NostrServerTransportConfig) -> Result<Self>
445    where
446        T: IntoNostrSigner,
447    {
448        let relay_pool: Arc<dyn RelayPoolTrait> =
449            Arc::new(RelayPool::new(signer).await.map_err(|error| {
450                tracing::error!(
451                    target: LOG_TARGET,
452                    error = %error,
453                    "Failed to initialize relay pool for server transport"
454                );
455                error
456            })?);
457        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
458        let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new(
459            NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
460        )));
461
462        tracing::info!(
463            target: LOG_TARGET,
464            relay_count = config.relay_urls.len(),
465            announced = config.is_announced_server,
466            encryption_mode = ?config.encryption_mode,
467            gift_wrap_mode = ?config.gift_wrap_mode,
468            "Created server transport"
469        );
470        let mut announcement_manager = announcement_manager::AnnouncementManager::new(
471            Arc::clone(&relay_pool),
472            config.server_info.clone(),
473            config.encryption_mode,
474            config.gift_wrap_mode,
475            tx.clone(),
476            config.relay_urls.clone(),
477            config.relay_list_urls.clone(),
478            config.bootstrap_relay_urls.clone(),
479            config.publish_relay_list,
480            config.profile_metadata.clone(),
481        );
482        // CEP-22 + CEP-41: advertise oversized-transfer and open-stream support in
483        // announcements + first responses (each gated by its own config flag).
484        announcement_manager.set_internal_common_tags(internal_common_capability_tags(&config));
485        Ok(Self {
486            announcement_manager,
487            base: BaseTransport {
488                relay_pool,
489                encryption_mode: config.encryption_mode,
490                is_connected: false,
491            },
492            sessions: SessionStore::with_capacity(config.max_sessions),
493            oversized_receiver: new_oversized_receiver_store(config.max_sessions),
494            open_stream: ServerOpenStreamState::new(&config.open_stream, config.max_sessions),
495            config,
496            event_routes: ServerEventRouteStore::new(),
497            request_wrap_kinds: Arc::new(RwLock::new(HashMap::new())),
498            seen_gift_wrap_ids,
499            message_tx: Some(tx),
500            message_rx: Some(rx),
501            cancellation_token: CancellationToken::new(),
502            task_handles: Vec::new(),
503        })
504    }
505
506    /// Like [`new`](Self::new) but accepts an existing relay pool.
507    pub async fn with_relay_pool(
508        config: NostrServerTransportConfig,
509        relay_pool: Arc<dyn RelayPoolTrait>,
510    ) -> Result<Self> {
511        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
512        let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new(
513            NonZeroUsize::new(DEFAULT_LRU_SIZE).expect("DEFAULT_LRU_SIZE must be non-zero"),
514        )));
515
516        tracing::info!(
517            target: LOG_TARGET,
518            relay_count = config.relay_urls.len(),
519            announced = config.is_announced_server,
520            encryption_mode = ?config.encryption_mode,
521            "Created server transport (with_relay_pool)"
522        );
523        let mut announcement_manager = announcement_manager::AnnouncementManager::new(
524            Arc::clone(&relay_pool),
525            config.server_info.clone(),
526            config.encryption_mode,
527            config.gift_wrap_mode,
528            tx.clone(),
529            config.relay_urls.clone(),
530            config.relay_list_urls.clone(),
531            config.bootstrap_relay_urls.clone(),
532            config.publish_relay_list,
533            config.profile_metadata.clone(),
534        );
535        // CEP-22 + CEP-41: advertise oversized-transfer and open-stream support in
536        // announcements + first responses (each gated by its own config flag).
537        announcement_manager.set_internal_common_tags(internal_common_capability_tags(&config));
538        Ok(Self {
539            announcement_manager,
540            base: BaseTransport {
541                relay_pool,
542                encryption_mode: config.encryption_mode,
543                is_connected: false,
544            },
545            sessions: SessionStore::with_capacity(config.max_sessions),
546            oversized_receiver: new_oversized_receiver_store(config.max_sessions),
547            open_stream: ServerOpenStreamState::new(&config.open_stream, config.max_sessions),
548            config,
549            request_wrap_kinds: Arc::new(RwLock::new(HashMap::new())),
550            event_routes: ServerEventRouteStore::new(),
551            seen_gift_wrap_ids,
552            message_tx: Some(tx),
553            message_rx: Some(rx),
554            cancellation_token: CancellationToken::new(),
555            task_handles: Vec::new(),
556        })
557    }
558
559    /// Start listening for incoming requests.
560    pub async fn start(&mut self) -> Result<()> {
561        self.base
562            .connect(&self.config.relay_urls)
563            .await
564            .map_err(|error| {
565                tracing::error!(
566                    target: LOG_TARGET,
567                    error = %error,
568                    "Failed to connect server transport to relays"
569                );
570                error
571            })?;
572
573        let pubkey = self.base.get_public_key().await.map_err(|error| {
574            tracing::error!(
575                target: LOG_TARGET,
576                error = %error,
577                "Failed to fetch server transport public key"
578            );
579            error
580        })?;
581        tracing::info!(
582            target: LOG_TARGET,
583            pubkey = %pubkey.to_hex(),
584            "Server transport started"
585        );
586
587        self.base
588            .subscribe_for_pubkey(&pubkey)
589            .await
590            .map_err(|error| {
591                tracing::error!(
592                    target: LOG_TARGET,
593                    error = %error,
594                    pubkey = %pubkey.to_hex(),
595                    "Failed to subscribe server transport for pubkey"
596                );
597                error
598            })?;
599
600        // Spawn event loop with cancellation support
601        let relay_pool = Arc::clone(&self.base.relay_pool);
602        let sessions = self.sessions.clone();
603        let event_routes = self.event_routes.clone();
604        let request_wrap_kinds = self.request_wrap_kinds.clone();
605        let tx = self
606            .message_tx
607            .as_ref()
608            .expect("message_tx must exist before start()")
609            .clone();
610        let allowed = self.config.allowed_public_keys.clone();
611        let excluded = self.config.excluded_capabilities.clone();
612        let encryption_mode = self.config.encryption_mode;
613        let gift_wrap_mode = self.config.gift_wrap_mode;
614        let is_announced_server = self.config.is_announced_server;
615        let oversized_enabled = self.config.oversized_transfer.enabled;
616        let oversized_receiver = self.oversized_receiver.clone();
617        let transfer_policy: TransferPolicy = (&self.config.oversized_transfer).into();
618        let common_tags_snapshot = self.announcement_manager.common_tags_snapshot();
619        let seen_gift_wrap_ids = self.seen_gift_wrap_ids.clone();
620        let open_stream = self.open_stream.clone();
621        let event_loop_token = self.cancellation_token.child_token();
622
623        let event_loop_handle = tokio::spawn(async move {
624            Self::event_loop(
625                relay_pool,
626                sessions,
627                event_routes,
628                request_wrap_kinds,
629                tx,
630                allowed,
631                excluded,
632                encryption_mode,
633                gift_wrap_mode,
634                is_announced_server,
635                oversized_enabled,
636                oversized_receiver,
637                transfer_policy,
638                common_tags_snapshot,
639                seen_gift_wrap_ids,
640                open_stream,
641                event_loop_token,
642            )
643            .await;
644        });
645
646        // Spawn session cleanup with cancellation support
647        let sessions_cleanup = self.sessions.clone();
648        let event_routes_cleanup = self.event_routes.clone();
649        let request_wrap_kinds_cleanup = self.request_wrap_kinds.clone();
650        let cleanup_interval = self.config.cleanup_interval;
651        let session_timeout = self.config.session_timeout;
652        let request_timeout = self.config.request_timeout;
653        let cleanup_token = self.cancellation_token.child_token();
654
655        let cleanup_handle = tokio::spawn(async move {
656            let mut interval = tokio::time::interval(cleanup_interval);
657            loop {
658                tokio::select! {
659                    _ = cleanup_token.cancelled() => {
660                        tracing::info!(
661                            target: LOG_TARGET,
662                            "Server cleanup task cancelled"
663                        );
664                        break;
665                    }
666                    _ = interval.tick() => {
667                        let cleaned = Self::cleanup_sessions(
668                            &sessions_cleanup,
669                            &event_routes_cleanup,
670                            &request_wrap_kinds_cleanup,
671                            session_timeout,
672                        )
673                        .await;
674                        if cleaned > 0 {
675                            tracing::info!(
676                                target: LOG_TARGET,
677                                cleaned_sessions = cleaned,
678                                "Cleaned up inactive sessions"
679                            );
680                        }
681                    }
682                }
683
684                // Sweep stale route entries in active sessions (rmcp handles timeout errors).
685                let swept_event_ids = event_routes_cleanup
686                    .sweep_stale_routes(request_timeout)
687                    .await;
688                if !swept_event_ids.is_empty() {
689                    let mut kinds_w = request_wrap_kinds_cleanup.write().await;
690                    for event_id in &swept_event_ids {
691                        kinds_w.remove(event_id);
692                    }
693                    drop(kinds_w);
694                    tracing::warn!(
695                        target: LOG_TARGET,
696                        swept = swept_event_ids.len(),
697                        timeout_secs = request_timeout.as_secs(),
698                        "Swept stale event routes (rmcp handles timeout errors)"
699                    );
700                }
701            }
702        });
703
704        self.task_handles.push(event_loop_handle);
705        self.task_handles.push(cleanup_handle);
706
707        tracing::info!(
708            target: LOG_TARGET,
709            relay_count = self.config.relay_urls.len(),
710            cleanup_interval_secs = self.config.cleanup_interval.as_secs(),
711            session_timeout_secs = self.config.session_timeout.as_secs(),
712            "Server transport loops spawned"
713        );
714        Ok(())
715    }
716
717    /// Close the transport — cancels event loop and cleanup tasks, then disconnects.
718    pub async fn close(&mut self) -> Result<()> {
719        self.cancellation_token.cancel();
720        for handle in self.task_handles.drain(..) {
721            let _ = handle.await;
722        }
723        self.announcement_manager.shutdown();
724        self.message_tx.take();
725        self.base.disconnect().await?;
726        self.sessions.clear().await;
727        self.event_routes.clear().await;
728        self.oversized_receiver.write().await.clear();
729        // CEP-41: dispose every inbound reader session and drop all writer /
730        // deferred-response state.
731        {
732            let mut receivers = self.open_stream.receiver.lock().await;
733            for (_, receiver) in receivers.iter_mut() {
734                receiver.clear();
735            }
736            receivers.clear();
737        }
738        self.open_stream.lock_slots().clear();
739        self.open_stream.lock_token_index().clear();
740        Ok(())
741    }
742
743    /// Send a response back to the client that sent the original request.
744    pub async fn send_response(&self, event_id: &str, mut response: JsonRpcMessage) -> Result<()> {
745        // CEP-41: response deferral. Decide BEFORE consuming the route — for a
746        // started stream the final response rides the captured snapshot, not the
747        // (possibly-swept) event route.
748        if self.open_stream.enabled {
749            match self.try_defer_open_stream_response(event_id, response) {
750                // Stashed (ordering A) — the close/abort hook flushes it later.
751                OpenStreamDeferral::Deferred => return Ok(()),
752                // Stream already closed (ordering B) — deliver from the snapshot now.
753                OpenStreamDeferral::SendNow { snapshot, response } => {
754                    return self
755                        .send_open_stream_deferred_response(event_id, &snapshot, response)
756                        .await;
757                }
758                // No active stream for this event — fall through to the normal path.
759                OpenStreamDeferral::Passthrough(returned) => response = returned,
760            }
761        }
762
763        // Consume the route up-front so only one concurrent responder can proceed
764        // for a given event_id.
765        let route = self.event_routes.pop(event_id).await.ok_or_else(|| {
766            tracing::error!(
767                target: LOG_TARGET,
768                event_id = %event_id,
769                "No client found for response correlation"
770            );
771            Error::Other(format!("No client found for event {event_id}"))
772        })?;
773
774        let client_pubkey_hex = route.client_pubkey;
775        let original_request_id = route.original_request_id;
776        let progress_token = route.progress_token;
777
778        let mut sessions_w = self.sessions.write().await;
779        let session = sessions_w.get_mut(&client_pubkey_hex).ok_or_else(|| {
780            tracing::error!(
781                target: LOG_TARGET,
782                client_pubkey = %client_pubkey_hex,
783                "No session for correlated client"
784            );
785            Error::Other(format!("No session for client {client_pubkey_hex}"))
786        })?;
787
788        // Restore original request ID
789        match &mut response {
790            JsonRpcMessage::Response(r) => r.id = original_request_id.clone(),
791            JsonRpcMessage::ErrorResponse(r) => r.id = original_request_id.clone(),
792            _ => {}
793        }
794
795        // CEP-22: serialize once, *after* id restoration. The threshold check and
796        // the oversized split must both derive from this exact post-restoration
797        // string so the client reassembles bytes whose `id` matches the digest.
798        let serialized = serde_json::to_string(&response)?;
799
800        let is_encrypted = session.is_encrypted;
801        // CEP-22: capture the peer's oversized support while the session lock is held.
802        let supports_oversized_transfer = session.supports_oversized_transfer;
803
804        // CEP-35: include discovery tags on first response to this client
805        let discovery_tags = self.take_pending_server_discovery_tags(session);
806        drop(sessions_w);
807
808        // CEP-19: Look up the incoming wrap kind for mirroring
809        let mirrored_wrap_kind = self
810            .request_wrap_kinds
811            .read()
812            .await
813            .get(event_id)
814            .copied()
815            .flatten();
816
817        let client_pubkey = PublicKey::from_hex(&client_pubkey_hex).map_err(|error| {
818            tracing::error!(
819                target: LOG_TARGET,
820                error = %error,
821                client_pubkey = %client_pubkey_hex,
822                "Invalid client pubkey in session map"
823            );
824            Error::Other(error.to_string())
825        })?;
826
827        let event_id_parsed = EventId::from_hex(event_id).map_err(|error| {
828            tracing::error!(
829                target: LOG_TARGET,
830                error = %error,
831                event_id = %event_id,
832                "Invalid event id while sending response"
833            );
834            Error::Other(error.to_string())
835        })?;
836
837        let base_tags = BaseTransport::create_response_tags(&client_pubkey, &event_id_parsed);
838        let tags = BaseTransport::compose_outbound_tags(&base_tags, &discovery_tags, &[]);
839        let gift_wrap_kind = Self::select_outbound_gift_wrap_kind(
840            self.config.gift_wrap_mode,
841            is_encrypted,
842            mirrored_wrap_kind,
843        );
844
845        // CEP-22: a response is eligible for oversized fragmentation only when the
846        // feature is enabled, the peer advertised support, and the request carried a
847        // progressToken to address the frames with.
848        let oversized_eligible = self.config.oversized_transfer.enabled
849            && progress_token.is_some()
850            && supports_oversized_transfer;
851        let threshold = self.config.oversized_transfer.threshold;
852
853        // CEP-22: relay size limits apply to the *published* Nostr event, so decide
854        // on the published byte size, not the raw payload (mirrors TS
855        // `measurePublishedMcpMessageSize`). The raw serialized length is a cheap
856        // lower bound: at/above the threshold the response is conclusively oversized
857        // and we fragment without building a single event — an escape-heavy payload
858        // could otherwise overflow NIP-44's plaintext limit while measuring. Below
859        // it, build the single event once, measure it, and reuse it when it fits.
860        let mut reuse_event: Option<Event> = None;
861        let fragment = if !oversized_eligible {
862            false
863        } else if serialized.len() >= threshold {
864            true
865        } else {
866            match self
867                .base
868                .prepare_mcp_message(
869                    &response,
870                    &client_pubkey,
871                    CTXVM_MESSAGES_KIND,
872                    tags.clone(),
873                    Some(is_encrypted),
874                    gift_wrap_kind,
875                )
876                .await
877            {
878                Ok((_id, publishable)) => {
879                    let published_len = serde_json::to_string(&publishable)
880                        .map(|s| s.len())
881                        .unwrap_or(usize::MAX);
882                    if published_len > threshold {
883                        true
884                    } else {
885                        reuse_event = Some(publishable);
886                        false
887                    }
888                }
889                // Could not build one event (e.g. NIP-44 plaintext overflow from an
890                // escape-heavy payload) → it cannot be sent as a single event.
891                Err(error) => {
892                    tracing::debug!(
893                        target: LOG_TARGET,
894                        error = %error,
895                        event_id = %event_id,
896                        "Single-event build failed; sending response as oversized transfer"
897                    );
898                    true
899                }
900            }
901        };
902
903        // Both paths converge on the cleanup tail below — neither early-returns on
904        // success.
905        let send_result: Result<()> = if fragment {
906            self.send_oversized_response(
907                &serialized,
908                progress_token.as_deref().unwrap_or_default(),
909                &client_pubkey,
910                &base_tags,
911                tags,
912                is_encrypted,
913                gift_wrap_kind,
914            )
915            .await
916        } else if let Some(publishable) = reuse_event {
917            // Reuse the event already built for the size check — no re-encryption.
918            self.base
919                .relay_pool
920                .publish_event(&publishable)
921                .await
922                .map(|_| ())
923        } else {
924            self.base
925                .send_mcp_message(
926                    &response,
927                    &client_pubkey,
928                    CTXVM_MESSAGES_KIND,
929                    tags,
930                    Some(is_encrypted),
931                    gift_wrap_kind,
932                )
933                .await
934                .map(|_| ())
935        };
936
937        if let Err(error) = send_result {
938            tracing::error!(
939                target: LOG_TARGET,
940                error = %error,
941                client_pubkey = %client_pubkey_hex,
942                event_id = %event_id,
943                "Failed to publish response message"
944            );
945
946            // Re-register route on publish failure so caller can retry.
947            self.event_routes
948                .register(
949                    event_id.to_string(),
950                    client_pubkey_hex,
951                    original_request_id,
952                    progress_token,
953                )
954                .await;
955
956            return Err(error);
957        }
958
959        // Clean up wrap-kind tracking
960        self.request_wrap_kinds.write().await.remove(event_id);
961
962        let mut sessions = self.sessions.write().await;
963        if let Some(session) = sessions.get_mut(&client_pubkey_hex) {
964            // Clean up progress token
965            if let Some(token) = progress_token {
966                session.pending_requests.remove(&token);
967            }
968            session.event_to_progress_token.remove(event_id);
969            session.pending_requests.remove(event_id);
970        }
971        drop(sessions);
972
973        tracing::debug!(
974            target: LOG_TARGET,
975            client_pubkey = %client_pubkey_hex,
976            event_id = %event_id,
977            encrypted = is_encrypted,
978            "Sent server response and cleaned correlation state"
979        );
980        Ok(())
981    }
982
983    /// CEP-41: clone the active writer for `event_id` so the rmcp worker can inject
984    /// it into the request's `extensions` typemap before dispatch. Returns
985    /// `None` when open-stream is disabled or no writer exists for this request.
986    #[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
987    pub(crate) fn get_open_stream_writer(&self, event_id: &str) -> Option<OpenStreamWriter> {
988        if !self.open_stream.enabled {
989            return None;
990        }
991        self.open_stream.writer_for(event_id)
992    }
993
994    /// CEP-41: decide how `send_response` should handle the final response for a
995    /// (possibly) streaming request. Run under the slots lock so the stash/flush
996    /// decision is consistent against the close/abort hook's `terminated` flag.
997    fn try_defer_open_stream_response(
998        &self,
999        event_id: &str,
1000        response: JsonRpcMessage,
1001    ) -> OpenStreamDeferral {
1002        let mut slots = self.open_stream.lock_slots();
1003        let Some(slot) = slots.get_mut(event_id) else {
1004            return OpenStreamDeferral::Passthrough(response);
1005        };
1006
1007        if !slot.writer.has_started() {
1008            // The request carried a progressToken but the tool never streamed.
1009            // Drop the writer and send normally (progress-token-conflict guard —
1010            // a deferred-but-never-closed stream would otherwise hang the response).
1011            let token = slot.writer.progress_token().to_string();
1012            slots.remove(event_id);
1013            drop(slots);
1014            self.open_stream.lock_token_index().remove(&token);
1015            return OpenStreamDeferral::Passthrough(response);
1016        }
1017
1018        if slot.terminated {
1019            // Ordering B (the common case): the stream already closed/aborted —
1020            // deliver now from the captured snapshot (the route may be swept).
1021            let snapshot = slot.snapshot.clone();
1022            let token = slot.writer.progress_token().to_string();
1023            slots.remove(event_id);
1024            drop(slots);
1025            self.open_stream.lock_token_index().remove(&token);
1026            OpenStreamDeferral::SendNow { snapshot, response }
1027        } else {
1028            // Ordering A: the stream is still open — hold the response; the
1029            // close/abort hook flushes it from the snapshot when the stream ends.
1030            slot.pending_response = Some(response);
1031            OpenStreamDeferral::Deferred
1032        }
1033    }
1034
1035    /// CEP-41: deliver a deferred final response from a captured [`RouteSnapshot`],
1036    /// never consulting `event_routes` (route-lifetime-independent; the route may already be gone).
1037    async fn send_open_stream_deferred_response(
1038        &self,
1039        event_id: &str,
1040        snapshot: &RouteSnapshot,
1041        response: JsonRpcMessage,
1042    ) -> Result<()> {
1043        Self::publish_open_stream_deferred_response(
1044            &self.base,
1045            self.config.gift_wrap_mode,
1046            event_id,
1047            snapshot,
1048            response,
1049        )
1050        .await
1051    }
1052
1053    /// CEP-41 (static): the actual deferred-response publish, callable from both the
1054    /// `&self` path and the writer's close/abort hook (which has no `self`).
1055    async fn publish_open_stream_deferred_response(
1056        base: &BaseTransport,
1057        gift_wrap_mode: GiftWrapMode,
1058        event_id: &str,
1059        snapshot: &RouteSnapshot,
1060        mut response: JsonRpcMessage,
1061    ) -> Result<()> {
1062        // Restore the original request id (the normal path restores it from the
1063        // popped route; here it comes from the snapshot).
1064        match &mut response {
1065            JsonRpcMessage::Response(r) => r.id = snapshot.original_request_id.clone(),
1066            JsonRpcMessage::ErrorResponse(r) => r.id = snapshot.original_request_id.clone(),
1067            _ => {}
1068        }
1069        let event_id_parsed = EventId::from_hex(event_id).map_err(|error| {
1070            Error::Other(format!("Invalid event id for deferred response: {error}"))
1071        })?;
1072        // Correlate via the `e` tag exactly like a normal response so the client's
1073        // correlation gate accepts it.
1074        let tags = BaseTransport::create_response_tags(&snapshot.client_pubkey, &event_id_parsed);
1075        let gift_wrap_kind = Self::select_outbound_gift_wrap_kind(
1076            gift_wrap_mode,
1077            snapshot.is_encrypted,
1078            snapshot.mirrored_wrap_kind,
1079        );
1080        base.send_mcp_message(
1081            &response,
1082            &snapshot.client_pubkey,
1083            CTXVM_MESSAGES_KIND,
1084            tags,
1085            Some(snapshot.is_encrypted),
1086            gift_wrap_kind,
1087        )
1088        .await
1089        .map(|_| ())
1090    }
1091
1092    /// CEP-41 (static): the writer's close/abort hook. Marks the stream terminal
1093    /// and, when the response already arrived (ordering A), flushes it from the
1094    /// snapshot. Ordering B leaves the terminal slot for `send_response`.
1095    async fn flush_open_stream_response(
1096        state: &ServerOpenStreamState,
1097        base: &BaseTransport,
1098        gift_wrap_mode: GiftWrapMode,
1099        event_id: &str,
1100    ) {
1101        let ready = {
1102            let mut slots = state.lock_slots();
1103            match slots.get_mut(event_id) {
1104                Some(slot) => {
1105                    slot.terminated = true;
1106                    slot.pending_response.take().map(|response| {
1107                        (
1108                            slot.snapshot.clone(),
1109                            slot.writer.progress_token().to_string(),
1110                            response,
1111                        )
1112                    })
1113                }
1114                None => None,
1115            }
1116        };
1117
1118        let Some((snapshot, token, response)) = ready else {
1119            // Ordering B: the response has not arrived yet. Leave the terminal slot
1120            // in place; `send_response` will deliver it from the snapshot.
1121            return;
1122        };
1123
1124        // Ordering A: the response was stashed before the stream closed — remove
1125        // the slot and deliver it now.
1126        state.lock_slots().remove(event_id);
1127        state.lock_token_index().remove(&token);
1128        if let Err(error) = Self::publish_open_stream_deferred_response(
1129            base,
1130            gift_wrap_mode,
1131            event_id,
1132            &snapshot,
1133            response,
1134        )
1135        .await
1136        {
1137            tracing::error!(
1138                target: LOG_TARGET,
1139                error = %error,
1140                event_id = %event_id,
1141                "Failed to flush deferred open-stream response"
1142            );
1143        }
1144    }
1145
1146    /// CEP-22: publish a response as an ordered oversized-transfer frame sequence.
1147    ///
1148    /// Splits the post-restoration `serialized` string into `start → chunks… →
1149    /// end` frames (digest and split both derived from that exact string) and
1150    /// publishes each as a `notifications/progress` event to `recipient`. The
1151    /// server never reserves the `accept` slot or waits for a handshake — it only
1152    /// fragments for peers already known to support the feature. One-shot
1153    /// discovery tags ride the `start` frame only (`start_tags`); every later
1154    /// frame carries bare recipient + `e`-tags (`base_tags`).
1155    #[allow(clippy::too_many_arguments)]
1156    async fn send_oversized_response(
1157        &self,
1158        serialized: &str,
1159        progress_token: &str,
1160        recipient: &PublicKey,
1161        base_tags: &[Tag],
1162        start_tags: Vec<Tag>,
1163        is_encrypted: bool,
1164        gift_wrap_kind: Option<u16>,
1165    ) -> Result<()> {
1166        // CEP-22: derive a per-chunk payload budget so every published frame stays
1167        // under the threshold even after the JSON-RPC envelope, signature, and
1168        // (when encrypted) gift-wrap expansion. Mirrors TS `resolveSafeOversizedChunkSize`.
1169        // Continuation (chunk) frames carry the response `p`+`e` tags (`base_tags`),
1170        // so size against those — not the bare recipient tag — or the budget would
1171        // be ~70 bytes optimistic.
1172        let chunk_size = resolve_safe_chunk_size(
1173            self.config.oversized_transfer.chunk_size,
1174            &self.base,
1175            recipient,
1176            base_tags,
1177            is_encrypted,
1178            Kind::Custom(gift_wrap_kind.unwrap_or(GIFT_WRAP_KIND)),
1179            self.config.oversized_transfer.threshold,
1180        )
1181        .await?;
1182        let options = OversizedSenderOptions::new(progress_token).with_chunk_size(chunk_size);
1183        let frames = build_oversized_frames(serialized, &options)?.into_ordered();
1184
1185        // Discovery tags ride the start frame; `take` yields them once, then the
1186        // remaining frames fall back to bare recipient + `e`-tags.
1187        let mut start_tags = Some(start_tags);
1188        for frame in frames {
1189            let tags = start_tags.take().unwrap_or_else(|| base_tags.to_vec());
1190            let message = JsonRpcMessage::Notification(frame);
1191            self.base
1192                .send_mcp_message(
1193                    &message,
1194                    recipient,
1195                    CTXVM_MESSAGES_KIND,
1196                    tags,
1197                    Some(is_encrypted),
1198                    gift_wrap_kind,
1199                )
1200                .await?;
1201        }
1202        Ok(())
1203    }
1204
1205    /// Send a notification to a specific client.
1206    pub async fn send_notification(
1207        &self,
1208        client_pubkey_hex: &str,
1209        notification: &JsonRpcMessage,
1210        correlated_event_id: Option<&str>,
1211    ) -> Result<()> {
1212        let mut sessions = self.sessions.write().await;
1213        let session = sessions
1214            .get_mut(client_pubkey_hex)
1215            .ok_or_else(|| Error::Other(format!("No session for {client_pubkey_hex}")))?;
1216        let is_encrypted = session.is_encrypted;
1217        let supports_ephemeral = session.supports_ephemeral_gift_wrap;
1218
1219        // CEP-35: include discovery tags on first message to this client
1220        let discovery_tags = self.take_pending_server_discovery_tags(session);
1221        drop(sessions);
1222
1223        let client_pubkey =
1224            PublicKey::from_hex(client_pubkey_hex).map_err(|e| Error::Other(e.to_string()))?;
1225
1226        let mut base_tags = BaseTransport::create_recipient_tags(&client_pubkey);
1227        if let Some(eid) = correlated_event_id {
1228            let event_id = EventId::from_hex(eid).map_err(|e| Error::Other(e.to_string()))?;
1229            base_tags.push(Tag::event(event_id));
1230        }
1231
1232        let tags = BaseTransport::compose_outbound_tags(&base_tags, &discovery_tags, &[]);
1233
1234        // CEP-19: Look up mirrored wrap kind from correlated request
1235        let correlated_wrap_kind = if let Some(event_id) = correlated_event_id {
1236            self.request_wrap_kinds
1237                .read()
1238                .await
1239                .get(event_id)
1240                .copied()
1241                .flatten()
1242        } else {
1243            None
1244        };
1245
1246        self.base
1247            .send_mcp_message(
1248                notification,
1249                &client_pubkey,
1250                CTXVM_MESSAGES_KIND,
1251                tags,
1252                Some(is_encrypted),
1253                Self::select_outbound_notification_gift_wrap_kind(
1254                    self.config.gift_wrap_mode,
1255                    is_encrypted,
1256                    correlated_wrap_kind,
1257                    supports_ephemeral,
1258                ),
1259            )
1260            .await?;
1261
1262        Ok(())
1263    }
1264
1265    /// Broadcast a notification to all initialized clients.
1266    pub async fn broadcast_notification(&self, notification: &JsonRpcMessage) -> Result<()> {
1267        let sessions = self.sessions.read().await;
1268        let initialized: Vec<String> = sessions
1269            .iter()
1270            .filter(|(_, s)| s.is_initialized)
1271            .map(|(k, _)| k.clone())
1272            .collect();
1273        drop(sessions);
1274
1275        for pubkey in initialized {
1276            if let Err(error) = self.send_notification(&pubkey, notification, None).await {
1277                tracing::error!(
1278                    target: LOG_TARGET,
1279                    error = %error,
1280                    client_pubkey = %pubkey,
1281                    "Failed to send notification"
1282                );
1283            }
1284        }
1285        Ok(())
1286    }
1287
1288    /// Take the message receiver for consuming incoming requests.
1289    pub fn take_message_receiver(
1290        &mut self,
1291    ) -> Option<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>> {
1292        self.message_rx.take()
1293    }
1294
1295    /// Read-only snapshot of a client's learned session state, or `None` if no
1296    /// session exists for that public key (hex). Exposes learned peer
1297    /// capabilities (encryption, ephemeral encryption, CEP-22 oversized transfer).
1298    pub async fn session_snapshot(&self, client_pubkey: &str) -> Option<SessionSnapshot> {
1299        self.sessions.get_session(client_pubkey).await
1300    }
1301
1302    /// Sets extra discovery tags to include in announcements and first-response discovery replay.
1303    pub fn set_announcement_extra_tags(&mut self, tags: Vec<Tag>) {
1304        self.announcement_manager.set_extra_common_tags(tags);
1305    }
1306
1307    /// Sets pricing tags to include in announcement/list events and capability list responses.
1308    pub fn set_announcement_pricing_tags(&mut self, tags: Vec<Tag>) {
1309        self.announcement_manager.set_pricing_tags(tags);
1310    }
1311
1312    /// Publish server announcement (kind 11316).
1313    pub async fn announce(&self) -> Result<EventId> {
1314        self.announcement_manager.announce().await
1315    }
1316
1317    /// Publish tools list (kind 11317).
1318    pub async fn publish_tools(&self, tools: Vec<serde_json::Value>) -> Result<EventId> {
1319        self.announcement_manager.publish_tools(tools).await
1320    }
1321
1322    /// Publish resources list (kind 11318).
1323    pub async fn publish_resources(&self, resources: Vec<serde_json::Value>) -> Result<EventId> {
1324        self.announcement_manager.publish_resources(resources).await
1325    }
1326
1327    /// Publish prompts list (kind 11320).
1328    pub async fn publish_prompts(&self, prompts: Vec<serde_json::Value>) -> Result<EventId> {
1329        self.announcement_manager.publish_prompts(prompts).await
1330    }
1331
1332    /// Publish resource templates list (kind 11319).
1333    pub async fn publish_resource_templates(
1334        &self,
1335        templates: Vec<serde_json::Value>,
1336    ) -> Result<EventId> {
1337        self.announcement_manager
1338            .publish_resource_templates(templates)
1339            .await
1340    }
1341
1342    /// Delete server announcements (NIP-09 kind 5).
1343    pub async fn delete_announcements(&self, reason: &str) -> Result<()> {
1344        self.announcement_manager.delete_announcements(reason).await
1345    }
1346
1347    /// Spawn the CEP-6 auto-publish task if `is_announced_server` is set.
1348    ///
1349    /// Called by the rmcp worker after `start()` — not in `start()` itself —
1350    /// because the auto-publish flow injects synthetic MCP requests that
1351    /// require an rmcp handler to produce responses.
1352    #[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
1353    pub(crate) fn spawn_announcements(&mut self) {
1354        if self.config.is_announced_server {
1355            let handle = self
1356                .announcement_manager
1357                .spawn_publish_public_announcements(self.cancellation_token.child_token());
1358            self.task_handles.push(handle);
1359        }
1360        self.spawn_discoverability_publication();
1361    }
1362
1363    /// Spawn profile metadata and relay-list publication for direct transport users.
1364    ///
1365    /// This publishes kind 0 and kind 10002 discoverability events when configured.
1366    /// It intentionally does not spawn CEP-6 capability announcement tasks because
1367    /// those inject synthetic MCP requests that require an rmcp worker.
1368    pub fn spawn_discoverability_publication(&mut self) {
1369        let handle = self.announcement_manager.spawn_publish_discoverability();
1370        self.task_handles.push(handle);
1371    }
1372
1373    /// Forward an announcement response to the announcement manager for publishing.
1374    ///
1375    /// Called by the worker when a response with the announcement sentinel ID arrives.
1376    #[cfg_attr(not(feature = "rmcp"), allow(dead_code))]
1377    pub(crate) async fn handle_announcement_response(
1378        &self,
1379        response: JsonRpcMessage,
1380    ) -> Result<()> {
1381        self.announcement_manager
1382            .handle_announcement_response(response)
1383            .await
1384    }
1385
1386    /// Publish tools list from rmcp typed tool descriptors.
1387    #[cfg(feature = "rmcp")]
1388    pub async fn publish_tools_typed(&self, tools: Vec<rmcp::model::Tool>) -> Result<EventId> {
1389        self.announcement_manager.publish_tools_typed(tools).await
1390    }
1391
1392    /// Publish resources list from rmcp typed resource descriptors.
1393    #[cfg(feature = "rmcp")]
1394    pub async fn publish_resources_typed(
1395        &self,
1396        resources: Vec<rmcp::model::Resource>,
1397    ) -> Result<EventId> {
1398        self.announcement_manager
1399            .publish_resources_typed(resources)
1400            .await
1401    }
1402
1403    /// Publish prompts list from rmcp typed prompt descriptors.
1404    #[cfg(feature = "rmcp")]
1405    pub async fn publish_prompts_typed(
1406        &self,
1407        prompts: Vec<rmcp::model::Prompt>,
1408    ) -> Result<EventId> {
1409        self.announcement_manager
1410            .publish_prompts_typed(prompts)
1411            .await
1412    }
1413
1414    /// Publish resource templates list from rmcp typed template descriptors.
1415    #[cfg(feature = "rmcp")]
1416    pub async fn publish_resource_templates_typed(
1417        &self,
1418        templates: Vec<rmcp::model::ResourceTemplate>,
1419    ) -> Result<EventId> {
1420        self.announcement_manager
1421            .publish_resource_templates_typed(templates)
1422            .await
1423    }
1424
1425    // ── CEP-35 discovery tag helpers ──────────────────────────────
1426
1427    /// One-shot: returns common tags if not yet sent to this client, empty otherwise.
1428    fn take_pending_server_discovery_tags(&self, session: &mut ClientSession) -> Vec<Tag> {
1429        if session.has_sent_common_tags {
1430            return vec![];
1431        }
1432        session.has_sent_common_tags = true;
1433        self.announcement_manager.get_common_tags()
1434    }
1435
1436    // ── Internal ────────────────────────────────────────────────
1437
1438    fn is_capability_excluded(
1439        excluded: &[CapabilityExclusion],
1440        method: &str,
1441        name: Option<&str>,
1442    ) -> bool {
1443        // Always allow fundamental MCP methods
1444        if method == "initialize" || method == "notifications/initialized" {
1445            return true;
1446        }
1447
1448        excluded.iter().any(|excl| {
1449            if excl.method != method {
1450                return false;
1451            }
1452            match (&excl.name, name) {
1453                (Some(excl_name), Some(req_name)) => excl_name == req_name,
1454                (None, _) => true, // method-only match
1455                _ => false,
1456            }
1457        })
1458    }
1459
1460    /// CEP-22: one watchdog sweep over the per-peer reassembly engines. Reaps
1461    /// transfers past their hard deadline — local-only (no abort frame is
1462    /// emitted): the requester's own timeout fails the call, and late frames
1463    /// are orphan-ignored — then drops now-empty receivers so long-gone peers
1464    /// stop pinning LRU slots (admission recreates them on demand).
1465    async fn sweep_oversized_receivers(
1466        oversized_receiver: &Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
1467    ) {
1468        let mut receivers = oversized_receiver.write().await;
1469        let mut empty_peers: Vec<String> = Vec::new();
1470        for (peer, receiver) in receivers.iter_mut() {
1471            for token in receiver.remove_expired() {
1472                tracing::warn!(
1473                    target: LOG_TARGET,
1474                    client_pubkey = %peer,
1475                    token = %token,
1476                    "Oversized transfer reaped by watchdog"
1477                );
1478            }
1479            if receiver.active_transfer_count() == 0 {
1480                empty_peers.push(peer.clone());
1481            }
1482        }
1483        // Keys collected first, popped after: never mutate mid-iteration.
1484        for peer in empty_peers {
1485            receivers.pop(&peer);
1486        }
1487    }
1488
1489    #[allow(clippy::too_many_arguments)]
1490    async fn event_loop(
1491        relay_pool: Arc<dyn RelayPoolTrait>,
1492        sessions: SessionStore,
1493        event_routes: ServerEventRouteStore,
1494        request_wrap_kinds: Arc<RwLock<HashMap<String, Option<u16>>>>,
1495        tx: tokio::sync::mpsc::UnboundedSender<IncomingRequest>,
1496        allowed_pubkeys: Vec<String>,
1497        excluded_capabilities: Vec<CapabilityExclusion>,
1498        encryption_mode: EncryptionMode,
1499        gift_wrap_mode: GiftWrapMode,
1500        is_announced_server: bool,
1501        oversized_enabled: bool,
1502        oversized_receiver: Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
1503        transfer_policy: TransferPolicy,
1504        common_tags_snapshot: announcement_manager::CommonTagsSnapshot,
1505        seen_gift_wrap_ids: Arc<Mutex<LruCache<EventId, ()>>>,
1506        open_stream: ServerOpenStreamState,
1507        cancel: CancellationToken,
1508    ) {
1509        let mut notifications = relay_pool.notifications();
1510
1511        // CEP-22: receiver-side watchdog sweep. Same clamp formula as the
1512        // client's correlation sweep; the arm is disabled entirely when the
1513        // feature is off or the deadline is 0 (no watchdog).
1514        let watchdog_enabled = oversized_enabled && transfer_policy.transfer_timeout_ms != 0;
1515        let sweep_interval = (Duration::from_millis(transfer_policy.transfer_timeout_ms) / 2)
1516            .clamp(Duration::from_secs(1), Duration::from_secs(30));
1517        let mut sweep_timer =
1518            tokio::time::interval_at(tokio::time::Instant::now() + sweep_interval, sweep_interval);
1519
1520        // CEP-41: keepalive sweep for server-as-reader sessions. Cadence = half the
1521        // idle timeout, clamped to [1s, 30s] (the idle→probe→abort machine only
1522        // needs sub-idle granularity). Armed only when open-stream is enabled.
1523        let open_stream_sweep_enabled =
1524            open_stream.enabled && open_stream.policy.idle_timeout_ms != 0;
1525        let open_stream_sweep_interval =
1526            (Duration::from_millis(open_stream.policy.idle_timeout_ms) / 2)
1527                .clamp(Duration::from_secs(1), Duration::from_secs(30));
1528        let mut open_stream_sweep_timer = tokio::time::interval_at(
1529            tokio::time::Instant::now() + open_stream_sweep_interval,
1530            open_stream_sweep_interval,
1531        );
1532
1533        loop {
1534            let notification = tokio::select! {
1535                _ = cancel.cancelled() => {
1536                    tracing::info!(
1537                        target: LOG_TARGET,
1538                        "Server event loop cancelled"
1539                    );
1540                    break;
1541                }
1542                _ = sweep_timer.tick(), if watchdog_enabled => {
1543                    Self::sweep_oversized_receivers(&oversized_receiver).await;
1544                    continue;
1545                }
1546                _ = open_stream_sweep_timer.tick(), if open_stream_sweep_enabled => {
1547                    Self::sweep_open_stream_sessions(
1548                        &open_stream,
1549                        &relay_pool,
1550                        encryption_mode,
1551                        gift_wrap_mode,
1552                    )
1553                    .await;
1554                    continue;
1555                }
1556                result = notifications.recv() => {
1557                    match result {
1558                        Ok(n) => n,
1559                        Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
1560                            tracing::warn!(
1561                                target: LOG_TARGET,
1562                                skipped = n,
1563                                "Relay broadcast lagged, skipping missed events"
1564                            );
1565                            continue;
1566                        }
1567                        Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
1568                    }
1569                }
1570            };
1571            if let RelayPoolNotification::Event { event, .. } = notification {
1572                let is_gift_wrap = event.kind == Kind::Custom(GIFT_WRAP_KIND)
1573                    || event.kind == Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND);
1574                let outer_kind: u16 = event.kind.as_u16();
1575
1576                // CEP-19: Drop gift-wraps that violate the configured gift-wrap mode
1577                if is_gift_wrap && !gift_wrap_mode.allows_kind(outer_kind) {
1578                    tracing::warn!(
1579                        target: LOG_TARGET,
1580                        event_id = %event.id.to_hex(),
1581                        event_kind = outer_kind,
1582                        configured_mode = ?gift_wrap_mode,
1583                        "Dropping gift-wrap because it violates gift_wrap_mode policy"
1584                    );
1585                    continue;
1586                }
1587
1588                let (content, sender_pubkey, event_id, is_encrypted, inner_tags) = if is_gift_wrap {
1589                    if encryption_mode == EncryptionMode::Disabled {
1590                        tracing::warn!(
1591                            target: LOG_TARGET,
1592                            event_id = %event.id.to_hex(),
1593                            sender_pubkey = %event.pubkey.to_hex(),
1594                            "Received encrypted message but encryption is disabled"
1595                        );
1596                        continue;
1597                    }
1598                    {
1599                        let guard = match seen_gift_wrap_ids.lock() {
1600                            Ok(g) => g,
1601                            Err(poisoned) => poisoned.into_inner(),
1602                        };
1603                        if guard.contains(&event.id) {
1604                            tracing::debug!(
1605                                target: LOG_TARGET,
1606                                event_id = %event.id.to_hex(),
1607                                "Skipping duplicate gift-wrap (outer id)"
1608                            );
1609                            continue;
1610                        }
1611                    }
1612                    // Single-layer NIP-44 decrypt (matches JS/TS SDK)
1613                    let signer = match relay_pool.signer().await {
1614                        Ok(s) => s,
1615                        Err(error) => {
1616                            tracing::error!(
1617                                target: LOG_TARGET,
1618                                error = %error,
1619                                "Failed to get signer"
1620                            );
1621                            continue;
1622                        }
1623                    };
1624                    match encryption::decrypt_gift_wrap_single_layer(&signer, &event).await {
1625                        Ok(decrypted_json) => {
1626                            // The decrypted content is JSON of the inner signed event.
1627                            // Use the INNER event's ID for correlation — the client
1628                            // registers the inner event ID in its correlation store.
1629                            match serde_json::from_str::<Event>(&decrypted_json) {
1630                                Ok(inner) => {
1631                                    if let Err(e) = inner.verify() {
1632                                        tracing::warn!(
1633                                            "Inner event signature verification failed: {e}"
1634                                        );
1635                                        continue;
1636                                    }
1637                                    {
1638                                        let mut guard = match seen_gift_wrap_ids.lock() {
1639                                            Ok(g) => g,
1640                                            Err(poisoned) => poisoned.into_inner(),
1641                                        };
1642                                        guard.put(event.id, ());
1643                                    }
1644                                    let inner_tags: Vec<Tag> = inner.tags.to_vec();
1645                                    (
1646                                        inner.content,
1647                                        inner.pubkey.to_hex(),
1648                                        inner.id.to_hex(),
1649                                        true,
1650                                        inner_tags,
1651                                    )
1652                                }
1653                                Err(error) => {
1654                                    tracing::error!(
1655                                        target: LOG_TARGET,
1656                                        error = %error,
1657                                        "Failed to parse inner event"
1658                                    );
1659                                    continue;
1660                                }
1661                            }
1662                        }
1663                        Err(error) => {
1664                            tracing::error!(
1665                                target: LOG_TARGET,
1666                                error = %error,
1667                                "Failed to decrypt"
1668                            );
1669                            continue;
1670                        }
1671                    }
1672                } else {
1673                    if encryption_mode == EncryptionMode::Required {
1674                        tracing::warn!(
1675                            target: LOG_TARGET,
1676                            sender_pubkey = %event.pubkey.to_hex(),
1677                            "Received unencrypted message but encryption is required"
1678                        );
1679                        continue;
1680                    }
1681                    (
1682                        event.content.clone(),
1683                        event.pubkey.to_hex(),
1684                        event.id.to_hex(),
1685                        false,
1686                        event.tags.to_vec(),
1687                    )
1688                };
1689
1690                // Parse MCP message
1691                let mcp_msg = match validation::validate_and_parse(&content) {
1692                    Some(msg) => msg,
1693                    None => {
1694                        tracing::warn!(
1695                            target: LOG_TARGET,
1696                            sender_pubkey = %sender_pubkey,
1697                            "Invalid MCP message"
1698                        );
1699                        continue;
1700                    }
1701                };
1702
1703                // Authorization check
1704                if !allowed_pubkeys.is_empty() {
1705                    let method = mcp_msg.method().unwrap_or("");
1706                    let name = match &mcp_msg {
1707                        JsonRpcMessage::Request(r) => r
1708                            .params
1709                            .as_ref()
1710                            .and_then(|p| p.get("name"))
1711                            .and_then(|n| n.as_str()),
1712                        _ => None,
1713                    };
1714
1715                    let is_excluded =
1716                        Self::is_capability_excluded(&excluded_capabilities, method, name);
1717
1718                    if !allowed_pubkeys.contains(&sender_pubkey) && !is_excluded {
1719                        tracing::warn!(
1720                            target: LOG_TARGET,
1721                            sender_pubkey = %sender_pubkey,
1722                            method = method,
1723                            "Unauthorized request"
1724                        );
1725
1726                        // Send a JSON-RPC error back for Request messages so the
1727                        // client doesn't hang indefinitely (announced servers only).
1728                        if is_announced_server {
1729                            if let JsonRpcMessage::Request(ref req) = mcp_msg {
1730                                if let Ok(client_pk) = PublicKey::from_hex(&sender_pubkey) {
1731                                    let event_id_parsed = EventId::from_hex(&event_id)
1732                                        .unwrap_or(EventId::all_zeros());
1733                                    let mut tags = BaseTransport::create_response_tags(
1734                                        &client_pk,
1735                                        &event_id_parsed,
1736                                    );
1737
1738                                    // CEP-19: Inject common discovery tags on first response
1739                                    let has_sent = sessions
1740                                        .get_session(&sender_pubkey)
1741                                        .await
1742                                        .is_some_and(|s| s.has_sent_common_tags);
1743                                    if !has_sent {
1744                                        common_tags_snapshot.append_common_response_tags(&mut tags);
1745                                        sessions.mark_common_tags_sent(&sender_pubkey).await;
1746                                    }
1747
1748                                    let error_response =
1749                                        JsonRpcMessage::ErrorResponse(JsonRpcErrorResponse {
1750                                            jsonrpc: "2.0".to_string(),
1751                                            id: req.id.clone(),
1752                                            error: JsonRpcError {
1753                                                code: -32000,
1754                                                message: "Unauthorized".to_string(),
1755                                                data: None,
1756                                            },
1757                                        });
1758
1759                                    let base = BaseTransport {
1760                                        relay_pool: Arc::clone(&relay_pool),
1761                                        encryption_mode,
1762                                        is_connected: true,
1763                                    };
1764                                    if let Err(e) = base
1765                                        .send_mcp_message(
1766                                            &error_response,
1767                                            &client_pk,
1768                                            CTXVM_MESSAGES_KIND,
1769                                            tags,
1770                                            Some(is_encrypted),
1771                                            Self::select_outbound_gift_wrap_kind(
1772                                                gift_wrap_mode,
1773                                                is_encrypted,
1774                                                if is_gift_wrap { Some(outer_kind) } else { None },
1775                                            ),
1776                                        )
1777                                        .await
1778                                    {
1779                                        tracing::error!(
1780                                            target: LOG_TARGET,
1781                                            error = %e,
1782                                            sender_pubkey = %sender_pubkey,
1783                                            "Failed to send unauthorized error response"
1784                                        );
1785                                    }
1786                                }
1787                            }
1788                        } // if is_announced_server
1789
1790                        continue;
1791                    }
1792                }
1793
1794                // Session management
1795                let on_evicted_cb = sessions.eviction_callback();
1796                let mut sessions_w = sessions.write().await;
1797                if !sessions_w.contains(&sender_pubkey) {
1798                    let evicted =
1799                        sessions_w.push(sender_pubkey.clone(), ClientSession::new(is_encrypted));
1800                    SessionStore::handle_eviction(
1801                        &sender_pubkey,
1802                        evicted,
1803                        &mut sessions_w,
1804                        on_evicted_cb.as_ref(),
1805                        &event_routes,
1806                    )
1807                    .await;
1808                }
1809                let session = sessions_w.get_mut(&sender_pubkey).unwrap();
1810                session.update_activity();
1811                session.is_encrypted = is_encrypted;
1812
1813                // CEP-19: Mark ephemeral support if client used kind 21059
1814                if is_gift_wrap && outer_kind == EPHEMERAL_GIFT_WRAP_KIND {
1815                    session.supports_ephemeral_gift_wrap = true;
1816                }
1817
1818                // CEP-35: learn client capabilities from inner event tags
1819                let discovered = learn_peer_capabilities(&inner_tags);
1820                session.supports_encryption |= discovered.supports_encryption;
1821                session.supports_ephemeral_encryption |= discovered.supports_ephemeral_encryption;
1822                // CEP-22: snapshot the flag BEFORE the learning gate mutates
1823                // it — the very `start` frame carries the client's support tag, so
1824                // without this snapshot the first transfer would never get an `accept`.
1825                let client_already_supported = session.supports_oversized_transfer;
1826                // CEP-22: only learn oversized support if it is enabled on this server.
1827                session.supports_oversized_transfer |=
1828                    oversized_enabled && discovered.supports_oversized_transfer;
1829                // CEP-41: learn the client's open-stream support (gated on enabled).
1830                // Captured AFTER the OR-learn so the very `start` frame that carries
1831                // the support tag still elicits an `accept`.
1832                session.supports_open_stream |=
1833                    open_stream.enabled && discovered.supports_open_stream;
1834                let client_supports_open_stream = session.supports_open_stream;
1835
1836                // CEP-22: intercept oversized-transfer frames before request
1837                // correlation/dispatch. A disabled server forwards raw progress
1838                // notifications as before.
1839                if oversized_enabled {
1840                    if let JsonRpcMessage::Notification(ref n) = mcp_msg {
1841                        if OversizedTransferReceiver::is_oversized_frame(n) {
1842                            drop(sessions_w);
1843                            Self::handle_oversized_frame(
1844                                n,
1845                                &sender_pubkey,
1846                                &event_id,
1847                                is_encrypted,
1848                                is_gift_wrap,
1849                                outer_kind,
1850                                client_already_supported,
1851                                &oversized_receiver,
1852                                transfer_policy,
1853                                &relay_pool,
1854                                encryption_mode,
1855                                gift_wrap_mode,
1856                                &event_routes,
1857                                &request_wrap_kinds,
1858                                &tx,
1859                                &open_stream,
1860                            )
1861                            .await;
1862                            continue;
1863                        }
1864                    }
1865                }
1866
1867                // CEP-41: intercept open-stream frames beside the oversized branch.
1868                // Type-disjoint from oversized (`is_open_stream_frame` vs
1869                // `is_oversized_frame` claim distinct `cvm.type`s), so order is
1870                // irrelevant. A disabled server forwards the raw notification.
1871                if open_stream.enabled {
1872                    if let JsonRpcMessage::Notification(ref n) = mcp_msg {
1873                        if OpenStreamReceiver::is_open_stream_frame(n) {
1874                            drop(sessions_w);
1875                            Self::handle_open_stream_frame(
1876                                &open_stream,
1877                                &relay_pool,
1878                                encryption_mode,
1879                                gift_wrap_mode,
1880                                n,
1881                                &sender_pubkey,
1882                                &event_id,
1883                                is_encrypted,
1884                                is_gift_wrap,
1885                                outer_kind,
1886                                client_supports_open_stream,
1887                            )
1888                            .await;
1889                            continue;
1890                        }
1891                    }
1892                }
1893
1894                // Track request for correlation
1895                if let JsonRpcMessage::Request(ref req) = mcp_msg {
1896                    let original_id = req.id.clone();
1897
1898                    // Extract progress token from _meta if present. String or
1899                    // number (rmcp issues numbers): without numeric acceptance
1900                    // the response eligibility gate in `send_response` never
1901                    // opens for rmcp clients. Normalized to its stringified form
1902                    // for routing and frame addressing (the wire keeps emitting
1903                    // string tokens).
1904                    let progress_token = req
1905                        .params
1906                        .as_ref()
1907                        .and_then(|p| p.get("_meta"))
1908                        .and_then(|m| m.get("progressToken"))
1909                        .and_then(progress_token_string);
1910
1911                    // Duplicate into session fields (kept for backward compat).
1912                    session
1913                        .pending_requests
1914                        .insert(event_id.clone(), original_id.clone());
1915                    if let Some(ref token) = progress_token {
1916                        session
1917                            .pending_requests
1918                            .insert(token.clone(), serde_json::json!(event_id));
1919                        session
1920                            .event_to_progress_token
1921                            .insert(event_id.clone(), token.clone());
1922                    }
1923
1924                    drop(sessions_w);
1925
1926                    // CEP-19: Record the incoming wrap kind for response mirroring
1927                    {
1928                        let mut kinds_w = request_wrap_kinds.write().await;
1929                        kinds_w.insert(
1930                            event_id.clone(),
1931                            if is_gift_wrap { Some(outer_kind) } else { None },
1932                        );
1933                    }
1934
1935                    // CEP-41: capture the route fields for the writer's snapshot
1936                    // BEFORE they are moved into the route store.
1937                    let writer_request_id = original_id.clone();
1938                    let writer_token = progress_token.clone();
1939
1940                    event_routes
1941                        .register(
1942                            event_id.clone(),
1943                            sender_pubkey.clone(),
1944                            original_id,
1945                            progress_token,
1946                        )
1947                        .await;
1948
1949                    // CEP-41: a `tools/call` carrying a progressToken gets a
1950                    // server→client writer, captured with a route snapshot (so the
1951                    // deferred response survives a route sweep) and injected into
1952                    // the tool via the rmcp request extensions.
1953                    if open_stream.enabled && req.method == "tools/call" {
1954                        if let Some(token) = writer_token {
1955                            Self::create_open_stream_writer(
1956                                &open_stream,
1957                                &relay_pool,
1958                                encryption_mode,
1959                                gift_wrap_mode,
1960                                &event_id,
1961                                &sender_pubkey,
1962                                &token,
1963                                writer_request_id,
1964                                is_encrypted,
1965                                if is_gift_wrap { Some(outer_kind) } else { None },
1966                            );
1967                        }
1968                    }
1969                } else {
1970                    drop(sessions_w);
1971                }
1972
1973                // Handle initialized notification (re-acquire for write)
1974                if let JsonRpcMessage::Notification(ref n) = mcp_msg {
1975                    if n.method == "notifications/initialized" {
1976                        let mut sessions_w2 = sessions.write().await;
1977                        if let Some(session) = sessions_w2.get_mut(&sender_pubkey) {
1978                            session.is_initialized = true;
1979                        }
1980                    }
1981                }
1982
1983                // Forward to consumer
1984                let _ = tx.send(IncomingRequest {
1985                    message: mcp_msg,
1986                    client_pubkey: sender_pubkey,
1987                    event_id,
1988                    is_encrypted,
1989                });
1990            }
1991        }
1992    }
1993
1994    /// CEP-22 server inbound: process one oversized-transfer frame.
1995    ///
1996    /// Emits an `accept` on the opening frame when the client's support is not yet
1997    /// known, feeds the frame to this peer's reassembler, and — on the
1998    /// `end` frame — registers a response route and dispatches the reassembled
1999    /// request as a synthetic [`IncomingRequest`] (keyed by the end frame's real
2000    /// carrying event id, collision-free against the reserved sentinels).
2001    #[allow(clippy::too_many_arguments)]
2002    async fn handle_oversized_frame(
2003        frame: &JsonRpcNotification,
2004        sender_pubkey: &str,
2005        event_id: &str,
2006        is_encrypted: bool,
2007        is_gift_wrap: bool,
2008        outer_kind: u16,
2009        client_already_supported: bool,
2010        oversized_receiver: &Arc<RwLock<LruCache<String, OversizedTransferReceiver>>>,
2011        transfer_policy: TransferPolicy,
2012        relay_pool: &Arc<dyn RelayPoolTrait>,
2013        encryption_mode: EncryptionMode,
2014        gift_wrap_mode: GiftWrapMode,
2015        event_routes: &ServerEventRouteStore,
2016        request_wrap_kinds: &Arc<RwLock<HashMap<String, Option<u16>>>>,
2017        tx: &tokio::sync::mpsc::UnboundedSender<IncomingRequest>,
2018        open_stream: &ServerOpenStreamState,
2019    ) {
2020        // The outer progressToken keys the transfer (needed for accept + route).
2021        // String or number — defensive only: every known sender stringifies
2022        // tokens into frames.
2023        let token = frame
2024            .params
2025            .as_ref()
2026            .and_then(|p| p.get("progressToken"))
2027            .and_then(progress_token_string);
2028
2029        // 1. Emit `accept` on the opening frame if support is not yet known.
2030        let is_start = frame
2031            .params
2032            .as_ref()
2033            .and_then(|p| p.get("cvm"))
2034            .and_then(OversizedFrame::from_cvm_value)
2035            .is_some_and(|f| matches!(f, OversizedFrame::Start { .. }));
2036        let issued_accept = is_start && !client_already_supported && token.is_some();
2037        if issued_accept {
2038            if let Some(ref token) = token {
2039                Self::emit_accept_frame(
2040                    token,
2041                    sender_pubkey,
2042                    event_id,
2043                    is_encrypted,
2044                    is_gift_wrap,
2045                    outer_kind,
2046                    relay_pool,
2047                    encryption_mode,
2048                    gift_wrap_mode,
2049                )
2050                .await;
2051            }
2052        }
2053
2054        // 2. Feed the frame to this peer's reassembler (process_frame is sync; the
2055        // write guard is held only across the sync call, never an await). When we
2056        // issued an `accept`, the sender reserved progress slot 2 and its chunks
2057        // begin at slot 3 — but we never *receive* an `accept`, so feed a synthetic
2058        // one into our own receiver to align chunk-slot tracking with the handshake
2059        // layout (otherwise the frontier sticks at slot 2 and chunks pile up as
2060        // out-of-order until the gap exceeds the window).
2061        let outcome = {
2062            let mut store = oversized_receiver.write().await;
2063            if !store.contains(sender_pubkey) {
2064                store.put(
2065                    sender_pubkey.to_string(),
2066                    OversizedTransferReceiver::with_policy(transfer_policy),
2067                );
2068            }
2069            let receiver = store.get_mut(sender_pubkey).unwrap();
2070            let outcome = receiver.process_frame(frame);
2071            if issued_accept && matches!(outcome, Ok(None)) {
2072                if let Some(ref token) = token {
2073                    if let Ok(accept) = OversizedFrame::Accept.into_progress_notification(
2074                        token,
2075                        ACCEPT_PROGRESS,
2076                        None,
2077                    ) {
2078                        let _ = receiver.process_frame(&accept);
2079                    }
2080                }
2081            }
2082            outcome
2083        };
2084
2085        match outcome {
2086            // start/accept/chunk consumed — nothing to dispatch yet.
2087            Ok(None) => {}
2088            // The `end` frame: reassembled request ready to dispatch.
2089            Ok(Some(message)) => {
2090                let original_id = message.id().cloned().unwrap_or(serde_json::Value::Null);
2091                // CEP-41: extract the writer info from the reassembled `tools/call`
2092                // before `message` is moved. The oversized reassembly path bypasses
2093                // the regular request path, so the writer must be created HERE too
2094                // (mirrors TS `handleIncomingRequest`, which oversized re-enters).
2095                let writer_token = match &message {
2096                    JsonRpcMessage::Request(req) if req.method == "tools/call" => req
2097                        .params
2098                        .as_ref()
2099                        .and_then(|p| p.get("_meta"))
2100                        .and_then(|m| m.get("progressToken"))
2101                        .and_then(progress_token_string),
2102                    _ => None,
2103                };
2104                let writer_request_id = original_id.clone();
2105                // Mirror the incoming wrap kind for the eventual response (CEP-19).
2106                {
2107                    let mut kinds_w = request_wrap_kinds.write().await;
2108                    kinds_w.insert(
2109                        event_id.to_string(),
2110                        if is_gift_wrap { Some(outer_kind) } else { None },
2111                    );
2112                }
2113                event_routes
2114                    .register(
2115                        event_id.to_string(),
2116                        sender_pubkey.to_string(),
2117                        original_id,
2118                        token,
2119                    )
2120                    .await;
2121                if open_stream.enabled {
2122                    if let Some(progress_token) = writer_token {
2123                        Self::create_open_stream_writer(
2124                            open_stream,
2125                            relay_pool,
2126                            encryption_mode,
2127                            gift_wrap_mode,
2128                            event_id,
2129                            sender_pubkey,
2130                            &progress_token,
2131                            writer_request_id,
2132                            is_encrypted,
2133                            if is_gift_wrap { Some(outer_kind) } else { None },
2134                        );
2135                    }
2136                }
2137                let _ = tx.send(IncomingRequest {
2138                    message,
2139                    client_pubkey: sender_pubkey.to_string(),
2140                    event_id: event_id.to_string(),
2141                    is_encrypted,
2142                });
2143            }
2144            // Clean up locally, let the peer's own timeout fire.
2145            Err(error) => {
2146                tracing::warn!(
2147                    target: LOG_TARGET,
2148                    error = %error,
2149                    sender_pubkey = %sender_pubkey,
2150                    "Oversized transfer frame rejected; cleaning up locally"
2151                );
2152            }
2153        }
2154    }
2155
2156    /// CEP-22: publish a single `accept` frame back to `sender_pubkey`, e-tagged to
2157    /// the `start` frame's carrying event. Best-effort — failures are logged only
2158    /// (the sender falls back to its own accept timeout).
2159    #[allow(clippy::too_many_arguments)]
2160    async fn emit_accept_frame(
2161        token: &str,
2162        sender_pubkey: &str,
2163        start_event_id: &str,
2164        is_encrypted: bool,
2165        is_gift_wrap: bool,
2166        outer_kind: u16,
2167        relay_pool: &Arc<dyn RelayPoolTrait>,
2168        encryption_mode: EncryptionMode,
2169        gift_wrap_mode: GiftWrapMode,
2170    ) {
2171        let client_pk = match PublicKey::from_hex(sender_pubkey) {
2172            Ok(pk) => pk,
2173            Err(_) => return,
2174        };
2175        let event_id_parsed = EventId::from_hex(start_event_id).unwrap_or(EventId::all_zeros());
2176        let accept = match OversizedFrame::Accept.into_progress_notification(
2177            token,
2178            ACCEPT_PROGRESS,
2179            Some("oversized request accepted"),
2180        ) {
2181            Ok(n) => JsonRpcMessage::Notification(n),
2182            Err(error) => {
2183                tracing::error!(
2184                    target: LOG_TARGET,
2185                    error = %error,
2186                    "Failed to build oversized-transfer accept frame"
2187                );
2188                return;
2189            }
2190        };
2191        let tags = BaseTransport::create_response_tags(&client_pk, &event_id_parsed);
2192        let base = BaseTransport {
2193            relay_pool: Arc::clone(relay_pool),
2194            encryption_mode,
2195            is_connected: true,
2196        };
2197        if let Err(error) = base
2198            .send_mcp_message(
2199                &accept,
2200                &client_pk,
2201                CTXVM_MESSAGES_KIND,
2202                tags,
2203                Some(is_encrypted),
2204                Self::select_outbound_gift_wrap_kind(
2205                    gift_wrap_mode,
2206                    is_encrypted,
2207                    if is_gift_wrap { Some(outer_kind) } else { None },
2208                ),
2209            )
2210            .await
2211        {
2212            tracing::error!(
2213                target: LOG_TARGET,
2214                error = %error,
2215                sender_pubkey = %sender_pubkey,
2216                "Failed to send oversized-transfer accept frame"
2217            );
2218        }
2219    }
2220
2221    /// CEP-41: create a server→client [`OpenStreamWriter`] for a `tools/call`
2222    /// carrying a `progressToken`, capture its [`RouteSnapshot`], register the
2223    /// `progress_token → event_id` index, and store the slot. The writer is later
2224    /// injected into the tool via the rmcp request `extensions`; its
2225    /// close/abort hooks flush the deferred final response from the snapshot.
2226    #[allow(clippy::too_many_arguments)]
2227    fn create_open_stream_writer(
2228        state: &ServerOpenStreamState,
2229        relay_pool: &Arc<dyn RelayPoolTrait>,
2230        encryption_mode: EncryptionMode,
2231        gift_wrap_mode: GiftWrapMode,
2232        event_id: &str,
2233        client_pubkey_hex: &str,
2234        progress_token: &str,
2235        original_request_id: serde_json::Value,
2236        is_encrypted: bool,
2237        mirrored_wrap_kind: Option<u16>,
2238    ) {
2239        let client_pubkey = match PublicKey::from_hex(client_pubkey_hex) {
2240            Ok(pk) => pk,
2241            Err(_) => return,
2242        };
2243        let event_id_parsed = match EventId::from_hex(event_id) {
2244            Ok(id) => id,
2245            Err(_) => return,
2246        };
2247        let gift_wrap_kind =
2248            Self::select_outbound_gift_wrap_kind(gift_wrap_mode, is_encrypted, mirrored_wrap_kind);
2249
2250        // Publish closure: every frame is e-tagged to the request event (so the
2251        // client can keep its pending correlation alive) and mirrors the inbound
2252        // gift-wrap kind (CEP-19). `send_notification`'s one-shot discovery tags
2253        // are not replayed — they already rode the initialize response / stream
2254        // start by the time a tool streams.
2255        let publish_relay_pool = Arc::clone(relay_pool);
2256        let publish_frame: PublishFrame = Arc::new(move |notification: JsonRpcNotification| {
2257            let relay_pool = Arc::clone(&publish_relay_pool);
2258            Box::pin(async move {
2259                let base = BaseTransport {
2260                    relay_pool,
2261                    encryption_mode,
2262                    is_connected: true,
2263                };
2264                let tags = BaseTransport::create_response_tags(&client_pubkey, &event_id_parsed);
2265                let message = JsonRpcMessage::Notification(notification);
2266                base.send_mcp_message(
2267                    &message,
2268                    &client_pubkey,
2269                    CTXVM_MESSAGES_KIND,
2270                    tags,
2271                    Some(is_encrypted),
2272                    gift_wrap_kind,
2273                )
2274                .await
2275            })
2276        });
2277
2278        // Terminal hooks flush any deferred final response from the snapshot.
2279        let on_close: OnCloseHook = {
2280            let state = state.clone();
2281            let relay_pool = Arc::clone(relay_pool);
2282            let event_id = event_id.to_string();
2283            Arc::new(move || {
2284                let state = state.clone();
2285                let relay_pool = Arc::clone(&relay_pool);
2286                let event_id = event_id.clone();
2287                Box::pin(async move {
2288                    let base = BaseTransport {
2289                        relay_pool,
2290                        encryption_mode,
2291                        is_connected: true,
2292                    };
2293                    Self::flush_open_stream_response(&state, &base, gift_wrap_mode, &event_id)
2294                        .await;
2295                })
2296            })
2297        };
2298        let on_abort: OnAbortHook = {
2299            let state = state.clone();
2300            let relay_pool = Arc::clone(relay_pool);
2301            let event_id = event_id.to_string();
2302            Arc::new(move |_reason| {
2303                let state = state.clone();
2304                let relay_pool = Arc::clone(&relay_pool);
2305                let event_id = event_id.clone();
2306                Box::pin(async move {
2307                    let base = BaseTransport {
2308                        relay_pool,
2309                        encryption_mode,
2310                        is_connected: true,
2311                    };
2312                    Self::flush_open_stream_response(&state, &base, gift_wrap_mode, &event_id)
2313                        .await;
2314                })
2315            })
2316        };
2317
2318        let writer = OpenStreamWriter::new(OpenStreamWriterOptions {
2319            progress_token: progress_token.to_string(),
2320            publish_frame,
2321            content_type: None,
2322            on_close: Some(on_close),
2323            on_abort: Some(on_abort),
2324        });
2325        let snapshot = RouteSnapshot {
2326            client_pubkey,
2327            original_request_id,
2328            is_encrypted,
2329            mirrored_wrap_kind,
2330        };
2331        state.lock_slots().insert(
2332            event_id.to_string(),
2333            OpenStreamSlot {
2334                writer,
2335                snapshot,
2336                pending_response: None,
2337                terminated: false,
2338            },
2339        );
2340        state
2341            .lock_token_index()
2342            .insert(progress_token.to_string(), event_id.to_string());
2343    }
2344
2345    /// CEP-41 inbound interception (beside the oversized branch). Routes control
2346    /// frames to the active writer (`ping → pong`, `abort → abort`) and otherwise
2347    /// drives the server-as-reader engine (`start`/`pong`/`chunk`/`close`).
2348    #[allow(clippy::too_many_arguments)]
2349    async fn handle_open_stream_frame(
2350        state: &ServerOpenStreamState,
2351        relay_pool: &Arc<dyn RelayPoolTrait>,
2352        encryption_mode: EncryptionMode,
2353        gift_wrap_mode: GiftWrapMode,
2354        notification: &JsonRpcNotification,
2355        sender_pubkey: &str,
2356        event_id: &str,
2357        is_encrypted: bool,
2358        is_gift_wrap: bool,
2359        outer_kind: u16,
2360        client_supports_open_stream: bool,
2361    ) {
2362        let token = notification
2363            .params
2364            .as_ref()
2365            .and_then(|p| p.get("progressToken"))
2366            .and_then(progress_token_string);
2367        // An active server→client writer owns this token's control frames.
2368        let writer = token
2369            .as_deref()
2370            .and_then(|t| state.event_id_for_token(t))
2371            .and_then(|eid| state.writer_for(&eid));
2372
2373        match open_stream_frame_from_notification(notification) {
2374            Some(OpenStreamFrame::Ping { nonce }) => {
2375                if let Some(writer) = writer {
2376                    let _ = writer.pong(nonce).await;
2377                } else {
2378                    Self::feed_open_stream_reader(
2379                        state,
2380                        relay_pool,
2381                        encryption_mode,
2382                        gift_wrap_mode,
2383                        notification,
2384                        sender_pubkey,
2385                        event_id,
2386                        is_encrypted,
2387                        is_gift_wrap,
2388                        outer_kind,
2389                    )
2390                    .await;
2391                }
2392            }
2393            Some(OpenStreamFrame::Abort { reason }) => {
2394                if let Some(writer) = writer {
2395                    let _ = writer.abort(reason).await;
2396                } else {
2397                    Self::feed_open_stream_reader(
2398                        state,
2399                        relay_pool,
2400                        encryption_mode,
2401                        gift_wrap_mode,
2402                        notification,
2403                        sender_pubkey,
2404                        event_id,
2405                        is_encrypted,
2406                        is_gift_wrap,
2407                        outer_kind,
2408                    )
2409                    .await;
2410                }
2411            }
2412            Some(OpenStreamFrame::Start { .. }) => {
2413                Self::feed_open_stream_reader(
2414                    state,
2415                    relay_pool,
2416                    encryption_mode,
2417                    gift_wrap_mode,
2418                    notification,
2419                    sender_pubkey,
2420                    event_id,
2421                    is_encrypted,
2422                    is_gift_wrap,
2423                    outer_kind,
2424                )
2425                .await;
2426                // Stateless accept: only for clients that advertised support.
2427                if client_supports_open_stream {
2428                    if let Some(token) = token.as_deref() {
2429                        Self::publish_open_stream_control_frame(
2430                            state,
2431                            relay_pool,
2432                            encryption_mode,
2433                            gift_wrap_mode,
2434                            OpenStreamFrame::Accept,
2435                            token,
2436                            sender_pubkey,
2437                            Some(event_id),
2438                            is_encrypted,
2439                            is_gift_wrap,
2440                            outer_kind,
2441                        )
2442                        .await;
2443                    }
2444                }
2445            }
2446            // pong / chunk / close / accept → server-as-reader engine.
2447            _ => {
2448                Self::feed_open_stream_reader(
2449                    state,
2450                    relay_pool,
2451                    encryption_mode,
2452                    gift_wrap_mode,
2453                    notification,
2454                    sender_pubkey,
2455                    event_id,
2456                    is_encrypted,
2457                    is_gift_wrap,
2458                    outer_kind,
2459                )
2460                .await;
2461            }
2462        }
2463    }
2464
2465    /// CEP-41 server-as-reader: feed an inbound frame to this peer's reader engine
2466    /// (created on demand) and publish a `pong` if its session asks for one.
2467    #[allow(clippy::too_many_arguments)]
2468    async fn feed_open_stream_reader(
2469        state: &ServerOpenStreamState,
2470        relay_pool: &Arc<dyn RelayPoolTrait>,
2471        encryption_mode: EncryptionMode,
2472        gift_wrap_mode: GiftWrapMode,
2473        notification: &JsonRpcNotification,
2474        sender_pubkey: &str,
2475        event_id: &str,
2476        is_encrypted: bool,
2477        is_gift_wrap: bool,
2478        outer_kind: u16,
2479    ) {
2480        let outcome = {
2481            let mut store = state.receiver.lock().await;
2482            if !store.contains(sender_pubkey) {
2483                store.put(
2484                    sender_pubkey.to_string(),
2485                    OpenStreamReceiver::with_policy(state.policy),
2486                );
2487            }
2488            let receiver = store
2489                .get_mut(sender_pubkey)
2490                .expect("open-stream receiver present after insert");
2491            receiver.process_frame(notification).await
2492        };
2493        match outcome {
2494            Ok(FrameOutcome::SendPong(nonce)) => {
2495                if let Some(token) = notification
2496                    .params
2497                    .as_ref()
2498                    .and_then(|p| p.get("progressToken"))
2499                    .and_then(progress_token_string)
2500                {
2501                    Self::publish_open_stream_control_frame(
2502                        state,
2503                        relay_pool,
2504                        encryption_mode,
2505                        gift_wrap_mode,
2506                        OpenStreamFrame::Pong { nonce },
2507                        &token,
2508                        sender_pubkey,
2509                        Some(event_id),
2510                        is_encrypted,
2511                        is_gift_wrap,
2512                        outer_kind,
2513                    )
2514                    .await;
2515                }
2516            }
2517            Ok(_) => {}
2518            Err(error) => {
2519                tracing::warn!(
2520                    target: LOG_TARGET,
2521                    error = %error,
2522                    sender_pubkey = %sender_pubkey,
2523                    "Inbound open-stream frame rejected by server reader engine"
2524                );
2525            }
2526        }
2527    }
2528
2529    /// CEP-41: publish one server→client control frame (`accept`/`pong`/`ping`) on
2530    /// the server-as-reader path, e-tagged to `correlated_event_id` and mirroring
2531    /// the inbound gift-wrap kind.
2532    #[allow(clippy::too_many_arguments)]
2533    async fn publish_open_stream_control_frame(
2534        state: &ServerOpenStreamState,
2535        relay_pool: &Arc<dyn RelayPoolTrait>,
2536        encryption_mode: EncryptionMode,
2537        gift_wrap_mode: GiftWrapMode,
2538        frame: OpenStreamFrame,
2539        token: &str,
2540        recipient_pubkey: &str,
2541        correlated_event_id: Option<&str>,
2542        is_encrypted: bool,
2543        is_gift_wrap: bool,
2544        outer_kind: u16,
2545    ) {
2546        let recipient = match PublicKey::from_hex(recipient_pubkey) {
2547            Ok(pk) => pk,
2548            Err(_) => return,
2549        };
2550        let progress = state.next_control_progress();
2551        let notification = match frame.into_progress_notification(token, progress, None) {
2552            Ok(n) => n,
2553            Err(error) => {
2554                tracing::error!(
2555                    target: LOG_TARGET,
2556                    error = %error,
2557                    "Failed to build open-stream control frame"
2558                );
2559                return;
2560            }
2561        };
2562        let mut tags = BaseTransport::create_recipient_tags(&recipient);
2563        // The `e`-tag is present only when the frame correlates to a known request
2564        // event (accept/pong reply to an inbound frame); the keepalive ping for a
2565        // server-as-reader session has no correlation and is sent recipient-only.
2566        if let Some(eid) = correlated_event_id.and_then(|id| EventId::from_hex(id).ok()) {
2567            tags.push(Tag::event(eid));
2568        }
2569        let base = BaseTransport {
2570            relay_pool: Arc::clone(relay_pool),
2571            encryption_mode,
2572            is_connected: true,
2573        };
2574        let gift_wrap_kind = Self::select_outbound_gift_wrap_kind(
2575            gift_wrap_mode,
2576            is_encrypted,
2577            if is_gift_wrap { Some(outer_kind) } else { None },
2578        );
2579        if let Err(error) = base
2580            .send_mcp_message(
2581                &JsonRpcMessage::Notification(notification),
2582                &recipient,
2583                CTXVM_MESSAGES_KIND,
2584                tags,
2585                Some(is_encrypted),
2586                gift_wrap_kind,
2587            )
2588            .await
2589        {
2590            tracing::warn!(
2591                target: LOG_TARGET,
2592                error = %error,
2593                "Failed to publish open-stream control frame"
2594            );
2595        }
2596    }
2597
2598    /// CEP-41: one keepalive sweep over the server-as-reader sessions (mirrors
2599    /// [`sweep_oversized_receivers`]). Drives each session's pure `tick`: idle →
2600    /// publish `ping`; probe/grace deadline → the reader aborted, so abort the
2601    /// paired writer too if one exists. Drops now-empty peer receivers.
2602    async fn sweep_open_stream_sessions(
2603        state: &ServerOpenStreamState,
2604        relay_pool: &Arc<dyn RelayPoolTrait>,
2605        encryption_mode: EncryptionMode,
2606        gift_wrap_mode: GiftWrapMode,
2607    ) {
2608        let now = Instant::now();
2609        let mut actions: Vec<(String, String, KeepaliveAction)> = Vec::new();
2610        {
2611            let mut store = state.receiver.lock().await;
2612            let mut empty_peers = Vec::new();
2613            for (peer, receiver) in store.iter_mut() {
2614                for (token, action) in receiver.registry_mut().tick_all(now) {
2615                    actions.push((peer.clone(), token, action));
2616                }
2617                if receiver.active_stream_count() == 0 {
2618                    empty_peers.push(peer.clone());
2619                }
2620            }
2621            for peer in empty_peers {
2622                store.pop(&peer);
2623            }
2624        }
2625
2626        let probe_is_encrypted = encryption_mode != EncryptionMode::Disabled;
2627        for (peer, token, action) in actions {
2628            match action {
2629                KeepaliveAction::SendPing(nonce) => {
2630                    // Server-as-reader sessions have no `token_to_event` entry
2631                    // (that index is only populated for server→client writers), so
2632                    // this ping is uncorrelated until bidirectional streaming wires
2633                    // a reader-side event id through.
2634                    let correlated = state.event_id_for_token(&token);
2635                    Self::publish_open_stream_control_frame(
2636                        state,
2637                        relay_pool,
2638                        encryption_mode,
2639                        gift_wrap_mode,
2640                        OpenStreamFrame::Ping { nonce },
2641                        &token,
2642                        &peer,
2643                        correlated.as_deref(),
2644                        probe_is_encrypted,
2645                        false,
2646                        0,
2647                    )
2648                    .await;
2649                }
2650                KeepaliveAction::Abort(reason) => {
2651                    if let Some(eid) = state.event_id_for_token(&token) {
2652                        if let Some(writer) = state.writer_for(&eid) {
2653                            let _ = writer.abort(Some(reason)).await;
2654                        }
2655                    }
2656                }
2657                KeepaliveAction::None => {}
2658            }
2659        }
2660    }
2661
2662    async fn cleanup_sessions(
2663        sessions: &SessionStore,
2664        event_routes: &ServerEventRouteStore,
2665        request_wrap_kinds: &Arc<RwLock<HashMap<String, Option<u16>>>>,
2666        timeout: Duration,
2667    ) -> usize {
2668        let mut sessions_w = sessions.write().await;
2669        let mut cleaned = 0;
2670        let mut stale_event_ids = Vec::new();
2671
2672        // LruCache has no retain(); collect expired keys then pop each one.
2673        let expired_keys: Vec<String> = sessions_w
2674            .iter()
2675            .filter(|(_, session)| session.last_activity.elapsed() > timeout)
2676            .map(|(k, _)| k.clone())
2677            .collect();
2678
2679        for key in &expired_keys {
2680            if let Some(session) = sessions_w.pop(key) {
2681                stale_event_ids.extend(session.pending_requests.keys().cloned());
2682                stale_event_ids.extend(session.event_to_progress_token.keys().cloned());
2683                tracing::debug!(
2684                    target: LOG_TARGET,
2685                    client_pubkey = %key,
2686                    "Session expired"
2687                );
2688                cleaned += 1;
2689            }
2690        }
2691        drop(sessions_w);
2692
2693        {
2694            let mut kinds_w = request_wrap_kinds.write().await;
2695            for event_id in &stale_event_ids {
2696                kinds_w.remove(event_id);
2697            }
2698        }
2699
2700        for event_id in &stale_event_ids {
2701            event_routes.pop(event_id).await;
2702        }
2703
2704        cleaned
2705    }
2706
2707    /// CEP-19: Choose outbound gift-wrap kind for responses.
2708    /// If `is_encrypted` is false, return None (send plaintext).
2709    /// Otherwise mirror the kind used by the client, falling back to the mode default.
2710    fn select_outbound_gift_wrap_kind(
2711        mode: GiftWrapMode,
2712        is_encrypted: bool,
2713        mirrored_kind: Option<u16>,
2714    ) -> Option<u16> {
2715        if !is_encrypted {
2716            return None;
2717        }
2718        if let Some(kind) = mirrored_kind {
2719            if mode.allows_kind(kind) {
2720                return Some(kind);
2721            }
2722        }
2723        match mode {
2724            GiftWrapMode::Persistent => Some(GIFT_WRAP_KIND),
2725            GiftWrapMode::Ephemeral => Some(EPHEMERAL_GIFT_WRAP_KIND),
2726            GiftWrapMode::Optional => Some(GIFT_WRAP_KIND),
2727        }
2728    }
2729
2730    /// CEP-19: Choose outbound gift-wrap kind for notifications.
2731    fn select_outbound_notification_gift_wrap_kind(
2732        mode: GiftWrapMode,
2733        is_encrypted: bool,
2734        correlated_wrap_kind: Option<u16>,
2735        client_supports_ephemeral: bool,
2736    ) -> Option<u16> {
2737        if !is_encrypted {
2738            return None;
2739        }
2740        // Mirror correlated request kind if available
2741        if let Some(kind) = correlated_wrap_kind {
2742            if mode.allows_kind(kind) {
2743                return Some(kind);
2744            }
2745        }
2746        // Fall back based on learned ephemeral support
2747        if client_supports_ephemeral && mode.supports_ephemeral() {
2748            return Some(EPHEMERAL_GIFT_WRAP_KIND);
2749        }
2750        match mode {
2751            GiftWrapMode::Persistent => Some(GIFT_WRAP_KIND),
2752            GiftWrapMode::Ephemeral => Some(EPHEMERAL_GIFT_WRAP_KIND),
2753            GiftWrapMode::Optional => Some(GIFT_WRAP_KIND),
2754        }
2755    }
2756}
2757
2758#[cfg(test)]
2759mod tests {
2760    use super::*;
2761    use crate::relay::mock::MockRelayPool;
2762    use std::thread;
2763
2764    // ── Session management ──────────────────────────────────────
2765
2766    #[test]
2767    fn test_client_session_creation() {
2768        let session = ClientSession::new(true);
2769        assert!(!session.is_initialized);
2770        assert!(session.is_encrypted);
2771        assert!(!session.has_sent_common_tags);
2772        assert!(!session.supports_ephemeral_gift_wrap);
2773        assert!(session.pending_requests.is_empty());
2774        assert!(session.event_to_progress_token.is_empty());
2775    }
2776
2777    #[test]
2778    fn test_client_session_update_activity() {
2779        let mut session = ClientSession::new(false);
2780        let first = session.last_activity;
2781        thread::sleep(Duration::from_millis(10));
2782        session.update_activity();
2783        assert!(session.last_activity > first);
2784    }
2785
2786    #[tokio::test]
2787    async fn test_cleanup_sessions_removes_expired() {
2788        let sessions = SessionStore::new();
2789        let event_routes = ServerEventRouteStore::new();
2790
2791        // Insert a session with an old activity time
2792        let mut session = ClientSession::new(false);
2793        session
2794            .pending_requests
2795            .insert("evt1".to_string(), serde_json::json!(1));
2796        sessions.write().await.put("pubkey1".to_string(), session);
2797        event_routes
2798            .register(
2799                "evt1".to_string(),
2800                "pubkey1".to_string(),
2801                serde_json::json!(1),
2802                None,
2803            )
2804            .await;
2805
2806        let request_wrap_kinds = Arc::new(RwLock::new(HashMap::new()));
2807
2808        // With a long timeout, nothing should be cleaned
2809        let cleaned = NostrServerTransport::cleanup_sessions(
2810            &sessions,
2811            &event_routes,
2812            &request_wrap_kinds,
2813            Duration::from_secs(300),
2814        )
2815        .await;
2816        assert_eq!(cleaned, 0);
2817        assert_eq!(sessions.session_count().await, 1);
2818
2819        // With zero timeout, it should be cleaned
2820        thread::sleep(Duration::from_millis(5));
2821        let cleaned = NostrServerTransport::cleanup_sessions(
2822            &sessions,
2823            &event_routes,
2824            &request_wrap_kinds,
2825            Duration::from_millis(1),
2826        )
2827        .await;
2828        assert_eq!(cleaned, 1);
2829        assert_eq!(sessions.session_count().await, 0);
2830        assert!(event_routes.pop("evt1").await.is_none());
2831    }
2832
2833    #[tokio::test]
2834    async fn test_cleanup_preserves_active_sessions() {
2835        let sessions = SessionStore::new();
2836        let event_routes = ServerEventRouteStore::new();
2837        let request_wrap_kinds = Arc::new(RwLock::new(HashMap::new()));
2838
2839        sessions
2840            .get_or_create_session("active", false, &event_routes)
2841            .await;
2842
2843        let cleaned = NostrServerTransport::cleanup_sessions(
2844            &sessions,
2845            &event_routes,
2846            &request_wrap_kinds,
2847            Duration::from_secs(300),
2848        )
2849        .await;
2850        assert_eq!(cleaned, 0);
2851        assert_eq!(sessions.session_count().await, 1);
2852    }
2853
2854    // ── Request ID correlation ──────────────────────────────────
2855
2856    #[test]
2857    fn test_pending_request_tracking() {
2858        let mut session = ClientSession::new(false);
2859        session
2860            .pending_requests
2861            .insert("event_abc".to_string(), serde_json::json!(42));
2862        assert_eq!(
2863            session.pending_requests.get("event_abc"),
2864            Some(&serde_json::json!(42))
2865        );
2866    }
2867
2868    #[test]
2869    fn test_progress_token_tracking() {
2870        let mut session = ClientSession::new(false);
2871        session
2872            .event_to_progress_token
2873            .insert("evt1".to_string(), "token1".to_string());
2874        session
2875            .pending_requests
2876            .insert("token1".to_string(), serde_json::json!("evt1"));
2877        assert_eq!(
2878            session.event_to_progress_token.get("evt1"),
2879            Some(&"token1".to_string())
2880        );
2881    }
2882
2883    // ── Authorization (is_capability_excluded) ──────────────────
2884
2885    #[test]
2886    fn test_initialize_always_excluded() {
2887        assert!(NostrServerTransport::is_capability_excluded(
2888            &[],
2889            "initialize",
2890            None
2891        ));
2892        assert!(NostrServerTransport::is_capability_excluded(
2893            &[],
2894            "notifications/initialized",
2895            None
2896        ));
2897    }
2898
2899    #[test]
2900    fn test_method_excluded_without_name() {
2901        let exclusions = vec![CapabilityExclusion {
2902            method: "tools/list".to_string(),
2903            name: None,
2904        }];
2905        assert!(NostrServerTransport::is_capability_excluded(
2906            &exclusions,
2907            "tools/list",
2908            None
2909        ));
2910        assert!(NostrServerTransport::is_capability_excluded(
2911            &exclusions,
2912            "tools/list",
2913            Some("anything")
2914        ));
2915    }
2916
2917    #[test]
2918    fn test_method_excluded_with_name() {
2919        let exclusions = vec![CapabilityExclusion {
2920            method: "tools/call".to_string(),
2921            name: Some("get_weather".to_string()),
2922        }];
2923        assert!(NostrServerTransport::is_capability_excluded(
2924            &exclusions,
2925            "tools/call",
2926            Some("get_weather")
2927        ));
2928        assert!(!NostrServerTransport::is_capability_excluded(
2929            &exclusions,
2930            "tools/call",
2931            Some("other_tool")
2932        ));
2933        assert!(!NostrServerTransport::is_capability_excluded(
2934            &exclusions,
2935            "tools/call",
2936            None
2937        ));
2938    }
2939
2940    #[test]
2941    fn test_non_excluded_method() {
2942        let exclusions = vec![CapabilityExclusion {
2943            method: "tools/list".to_string(),
2944            name: None,
2945        }];
2946        assert!(!NostrServerTransport::is_capability_excluded(
2947            &exclusions,
2948            "tools/call",
2949            None
2950        ));
2951        assert!(!NostrServerTransport::is_capability_excluded(
2952            &exclusions,
2953            "resources/list",
2954            None
2955        ));
2956    }
2957
2958    #[test]
2959    fn test_empty_exclusions_non_init_method() {
2960        assert!(!NostrServerTransport::is_capability_excluded(
2961            &[],
2962            "tools/list",
2963            None
2964        ));
2965        assert!(!NostrServerTransport::is_capability_excluded(
2966            &[],
2967            "tools/call",
2968            Some("x")
2969        ));
2970    }
2971
2972    // ── Encryption mode enforcement ─────────────────────────────
2973
2974    #[test]
2975    fn test_encryption_mode_default() {
2976        let config = NostrServerTransportConfig::default();
2977        assert_eq!(config.encryption_mode, EncryptionMode::Optional);
2978    }
2979
2980    // ── Config defaults ─────────────────────────────────────────
2981
2982    #[test]
2983    fn test_config_defaults() {
2984        let config = NostrServerTransportConfig::default();
2985        assert_eq!(config.relay_urls, vec!["wss://relay.damus.io".to_string()]);
2986        assert!(!config.is_announced_server);
2987        assert_eq!(config.gift_wrap_mode, GiftWrapMode::Optional);
2988        assert!(config.allowed_public_keys.is_empty());
2989        assert!(config.excluded_capabilities.is_empty());
2990        assert_eq!(config.max_sessions, 1000);
2991        assert_eq!(config.cleanup_interval, Duration::from_secs(60));
2992        assert_eq!(config.session_timeout, Duration::from_secs(300));
2993        assert_eq!(config.request_timeout, Duration::from_secs(60));
2994        assert!(config.server_info.is_none());
2995        assert!(config.relay_list_urls.is_none());
2996        assert!(config.bootstrap_relay_urls.is_none());
2997        assert!(config.publish_relay_list);
2998        assert!(config.profile_metadata.is_none());
2999    }
3000
3001    #[tokio::test]
3002    async fn spawn_discoverability_publication_publishes_kind_0_and_10002_only() {
3003        let pool = Arc::new(MockRelayPool::new());
3004        let relay_pool: Arc<dyn RelayPoolTrait> = pool.clone();
3005        let config = NostrServerTransportConfig::default()
3006            .with_relay_urls(vec!["wss://relay.example.com".to_string()])
3007            .with_profile_metadata(ProfileMetadata::default().with_name("ffi-server"))
3008            .with_publish_relay_list(true);
3009        let mut transport = NostrServerTransport::with_relay_pool(config, relay_pool)
3010            .await
3011            .expect("transport should build");
3012
3013        transport.spawn_discoverability_publication();
3014        for handle in transport.task_handles.drain(..) {
3015            handle.await.expect("discoverability task should not panic");
3016        }
3017
3018        let events = pool.stored_events().await;
3019        assert!(
3020            events.iter().any(|e| e.kind == Kind::Custom(0)),
3021            "profile metadata should be published"
3022        );
3023        assert!(
3024            events
3025                .iter()
3026                .any(|e| e.kind == Kind::Custom(RELAY_LIST_METADATA_KIND)),
3027            "relay list should be published"
3028        );
3029        assert!(
3030            events
3031                .iter()
3032                .all(|e| e.kind != Kind::Custom(SERVER_ANNOUNCEMENT_KIND)),
3033            "direct discoverability publication must not emit CEP-6 announcements"
3034        );
3035    }
3036
3037    // ── CEP-19 helper logic ──────────────────────────────────────
3038
3039    #[test]
3040    fn test_select_outbound_gift_wrap_kind_plaintext() {
3041        assert_eq!(
3042            NostrServerTransport::select_outbound_gift_wrap_kind(
3043                GiftWrapMode::Optional,
3044                false,
3045                Some(GIFT_WRAP_KIND),
3046            ),
3047            None
3048        );
3049    }
3050
3051    #[test]
3052    fn test_select_outbound_gift_wrap_kind_mirrors_incoming() {
3053        assert_eq!(
3054            NostrServerTransport::select_outbound_gift_wrap_kind(
3055                GiftWrapMode::Optional,
3056                true,
3057                Some(EPHEMERAL_GIFT_WRAP_KIND),
3058            ),
3059            Some(EPHEMERAL_GIFT_WRAP_KIND)
3060        );
3061    }
3062
3063    #[test]
3064    fn test_select_outbound_gift_wrap_kind_persistent_mode_overrides_ephemeral() {
3065        assert_eq!(
3066            NostrServerTransport::select_outbound_gift_wrap_kind(
3067                GiftWrapMode::Persistent,
3068                true,
3069                Some(EPHEMERAL_GIFT_WRAP_KIND),
3070            ),
3071            Some(GIFT_WRAP_KIND)
3072        );
3073    }
3074
3075    #[test]
3076    fn test_append_common_response_tags_includes_encryption_when_optional() {
3077        let snapshot = announcement_manager::CommonTagsSnapshot {
3078            server_info: None,
3079            extra_common_tags: vec![],
3080            internal_common_tags: vec![],
3081            encryption_mode: EncryptionMode::Optional,
3082            gift_wrap_mode: GiftWrapMode::Optional,
3083        };
3084        let mut tags = Vec::new();
3085        snapshot.append_common_response_tags(&mut tags);
3086        let kinds: Vec<String> = tags.iter().map(|t| format!("{:?}", t.kind())).collect();
3087        assert!(
3088            kinds.iter().any(|k| k.contains("support_encryption")),
3089            "should include support_encryption tag"
3090        );
3091    }
3092
3093    #[test]
3094    fn test_append_common_response_tags_no_encryption_when_disabled() {
3095        let snapshot = announcement_manager::CommonTagsSnapshot {
3096            server_info: None,
3097            extra_common_tags: vec![],
3098            internal_common_tags: vec![],
3099            encryption_mode: EncryptionMode::Disabled,
3100            gift_wrap_mode: GiftWrapMode::Optional,
3101        };
3102        let mut tags = Vec::new();
3103        snapshot.append_common_response_tags(&mut tags);
3104        assert!(
3105            tags.is_empty(),
3106            "should not include encryption tags when encryption disabled"
3107        );
3108    }
3109
3110    #[test]
3111    fn test_select_outbound_notification_gift_wrap_kind_plaintext() {
3112        assert_eq!(
3113            NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3114                GiftWrapMode::Optional,
3115                false,
3116                Some(EPHEMERAL_GIFT_WRAP_KIND),
3117                true,
3118            ),
3119            None
3120        );
3121    }
3122
3123    #[test]
3124    fn test_select_outbound_notification_gift_wrap_kind_mirrors_correlated() {
3125        assert_eq!(
3126            NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3127                GiftWrapMode::Optional,
3128                true,
3129                Some(EPHEMERAL_GIFT_WRAP_KIND),
3130                false,
3131            ),
3132            Some(EPHEMERAL_GIFT_WRAP_KIND)
3133        );
3134    }
3135
3136    #[test]
3137    fn test_select_outbound_notification_gift_wrap_kind_falls_back_to_mode_if_correlated_not_allowed(
3138    ) {
3139        assert_eq!(
3140            NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3141                GiftWrapMode::Ephemeral,
3142                true,
3143                Some(GIFT_WRAP_KIND),
3144                false,
3145            ),
3146            Some(EPHEMERAL_GIFT_WRAP_KIND)
3147        );
3148    }
3149
3150    #[test]
3151    fn test_select_outbound_notification_gift_wrap_kind_uses_ephemeral_if_supported() {
3152        assert_eq!(
3153            NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3154                GiftWrapMode::Optional,
3155                true,
3156                None,
3157                true,
3158            ),
3159            Some(EPHEMERAL_GIFT_WRAP_KIND)
3160        );
3161    }
3162
3163    #[test]
3164    fn test_select_outbound_notification_gift_wrap_kind_uses_persistent_if_ephemeral_supported_but_mode_persistent(
3165    ) {
3166        assert_eq!(
3167            NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3168                GiftWrapMode::Persistent,
3169                true,
3170                None,
3171                true,
3172            ),
3173            Some(GIFT_WRAP_KIND)
3174        );
3175    }
3176
3177    #[test]
3178    fn test_select_outbound_notification_gift_wrap_kind_uses_default_mode_if_ephemeral_not_supported(
3179    ) {
3180        assert_eq!(
3181            NostrServerTransport::select_outbound_notification_gift_wrap_kind(
3182                GiftWrapMode::Optional,
3183                true,
3184                None,
3185                false,
3186            ),
3187            Some(GIFT_WRAP_KIND)
3188        );
3189    }
3190
3191    #[test]
3192    fn test_append_common_response_tags_includes_ephemeral_tag() {
3193        let snapshot = announcement_manager::CommonTagsSnapshot {
3194            server_info: None,
3195            extra_common_tags: vec![],
3196            internal_common_tags: vec![],
3197            encryption_mode: EncryptionMode::Optional,
3198            gift_wrap_mode: GiftWrapMode::Optional,
3199        };
3200        let mut tags = Vec::new();
3201        snapshot.append_common_response_tags(&mut tags);
3202        let kinds: Vec<String> = tags.iter().map(|t| format!("{:?}", t.kind())).collect();
3203        assert!(
3204            kinds
3205                .iter()
3206                .any(|k| k.contains("support_encryption_ephemeral")),
3207            "should include support_encryption_ephemeral tag"
3208        );
3209    }
3210
3211    #[test]
3212    fn test_append_common_response_tags_includes_server_info() {
3213        let server_info = ServerInfo {
3214            name: Some("TestServer".to_string()),
3215            ..Default::default()
3216        };
3217        let snapshot = announcement_manager::CommonTagsSnapshot {
3218            server_info: Some(server_info),
3219            extra_common_tags: vec![],
3220            internal_common_tags: vec![],
3221            encryption_mode: EncryptionMode::Disabled,
3222            gift_wrap_mode: GiftWrapMode::Optional,
3223        };
3224        let mut tags = Vec::new();
3225        snapshot.append_common_response_tags(&mut tags);
3226        let tag_value = tags
3227            .iter()
3228            .find(|t| (*t).clone().to_vec().first().map(|s| s.as_str()) == Some("name"))
3229            .and_then(|t| t.clone().to_vec().get(1).cloned());
3230        assert_eq!(tag_value.as_deref(), Some("TestServer"));
3231    }
3232
3233    #[test]
3234    fn test_append_common_response_tags_extra_tags() {
3235        let extra_tags = vec![Tag::custom(
3236            TagKind::Custom("custom_tag".into()),
3237            vec!["value".to_string()],
3238        )];
3239        let snapshot = announcement_manager::CommonTagsSnapshot {
3240            server_info: None,
3241            extra_common_tags: extra_tags,
3242            internal_common_tags: vec![],
3243            encryption_mode: EncryptionMode::Disabled,
3244            gift_wrap_mode: GiftWrapMode::Optional,
3245        };
3246        let mut tags = Vec::new();
3247        snapshot.append_common_response_tags(&mut tags);
3248        let tag_value = tags
3249            .iter()
3250            .find(|t| (*t).clone().to_vec().first().map(|s| s.as_str()) == Some("custom_tag"))
3251            .and_then(|t| t.clone().to_vec().get(1).cloned());
3252        assert_eq!(tag_value.as_deref(), Some("value"));
3253    }
3254
3255    // ── CEP-35 discovery tag helpers ────────────────────────────
3256
3257    #[test]
3258    fn test_cep35_client_session_new_fields_default_false() {
3259        let session = ClientSession::new(false);
3260        assert!(!session.has_sent_common_tags);
3261        assert!(!session.supports_encryption);
3262        assert!(!session.supports_ephemeral_encryption);
3263        assert!(!session.supports_oversized_transfer);
3264    }
3265
3266    #[test]
3267    fn test_cep35_capability_or_assign() {
3268        let mut session = ClientSession::new(false);
3269
3270        session.supports_encryption |= true;
3271        session.supports_ephemeral_encryption |= false;
3272
3273        session.supports_encryption |= false;
3274        session.supports_ephemeral_encryption |= true;
3275
3276        assert!(session.supports_encryption, "OR-assign must not downgrade");
3277        assert!(session.supports_ephemeral_encryption);
3278        assert!(!session.supports_oversized_transfer);
3279    }
3280
3281    #[test]
3282    fn test_config_gift_wrap_mode_default() {
3283        let config = NostrServerTransportConfig::default();
3284        assert_eq!(config.gift_wrap_mode, GiftWrapMode::Optional);
3285    }
3286
3287    // ── CEP-22 oversized transfer capability advertisement ──────
3288
3289    fn first_tag_values(tags: &[Tag]) -> Vec<String> {
3290        tags.iter().map(|t| t.clone().to_vec()[0].clone()).collect()
3291    }
3292
3293    async fn make_server_with_oversized(enabled: bool) -> NostrServerTransport {
3294        let config = NostrServerTransportConfig {
3295            oversized_transfer: OversizedTransferConfig::default().with_enabled(enabled),
3296            ..Default::default()
3297        };
3298        let pool: Arc<dyn RelayPoolTrait> = Arc::new(crate::relay::mock::MockRelayPool::new());
3299        NostrServerTransport::with_relay_pool(config, pool)
3300            .await
3301            .expect("server transport construction")
3302    }
3303
3304    #[test]
3305    fn test_oversized_enabled_by_default() {
3306        let config = NostrServerTransportConfig::default();
3307        assert!(config.oversized_transfer.enabled);
3308    }
3309
3310    #[test]
3311    fn test_oversized_support_tags_helper() {
3312        // Start from an explicit opt-out: the default is now enabled.
3313        let mut config = NostrServerTransportConfig::default().with_oversized_enabled(false);
3314        assert!(oversized_support_tags(&config).is_empty());
3315        config.oversized_transfer.enabled = true;
3316        let names = first_tag_values(&oversized_support_tags(&config));
3317        assert_eq!(names, vec!["support_oversized_transfer"]);
3318    }
3319
3320    #[test]
3321    fn test_oversized_builders() {
3322        let config = NostrServerTransportConfig::default().with_oversized_enabled(true);
3323        assert!(config.oversized_transfer.enabled);
3324        let config = NostrServerTransportConfig::default()
3325            .with_oversized_transfer(OversizedTransferConfig::enabled().with_threshold(123));
3326        assert!(config.oversized_transfer.enabled);
3327        assert_eq!(config.oversized_transfer.threshold, 123);
3328    }
3329
3330    #[tokio::test]
3331    async fn test_announcement_includes_oversized_tag_when_enabled() {
3332        let server = make_server_with_oversized(true).await;
3333        let names = first_tag_values(&server.announcement_manager.get_common_tags());
3334        assert!(
3335            names.contains(&"support_oversized_transfer".to_string()),
3336            "announcement common tags must advertise oversized support when enabled"
3337        );
3338    }
3339
3340    #[tokio::test]
3341    async fn test_announcement_omits_oversized_tag_when_disabled() {
3342        let server = make_server_with_oversized(false).await;
3343        let names = first_tag_values(&server.announcement_manager.get_common_tags());
3344        assert!(
3345            !names.contains(&"support_oversized_transfer".to_string()),
3346            "announcement must not advertise oversized support when disabled"
3347        );
3348    }
3349
3350    #[tokio::test]
3351    async fn test_first_response_snapshot_includes_oversized_tag_when_enabled() {
3352        let server = make_server_with_oversized(true).await;
3353        let snapshot = server.announcement_manager.common_tags_snapshot();
3354        let mut tags = Vec::new();
3355        snapshot.append_common_response_tags(&mut tags);
3356        let names = first_tag_values(&tags);
3357        assert!(
3358            names.contains(&"support_oversized_transfer".to_string()),
3359            "first-response replay must carry the oversized tag when enabled"
3360        );
3361    }
3362
3363    #[tokio::test]
3364    async fn test_first_response_snapshot_omits_oversized_tag_when_disabled() {
3365        let server = make_server_with_oversized(false).await;
3366        let snapshot = server.announcement_manager.common_tags_snapshot();
3367        let mut tags = Vec::new();
3368        snapshot.append_common_response_tags(&mut tags);
3369        let names = first_tag_values(&tags);
3370        assert!(!names.contains(&"support_oversized_transfer".to_string()));
3371    }
3372
3373    #[test]
3374    fn test_server_learns_client_oversized_only_when_enabled() {
3375        // Unit-level check that `learn_peer_capabilities` parses the client tag and
3376        // the `enabled && supports` truth table holds. The production gate in
3377        // `event_loop` is exercised end-to-end by the integration tests
3378        // `server_gate_allows_oversized_when_enabled` /
3379        // `server_gate_blocks_oversized_when_disabled` in tests/transport_integration.rs.
3380        let oversized_tag = Tag::custom(
3381            TagKind::Custom(tags::SUPPORT_OVERSIZED_TRANSFER.into()),
3382            Vec::<String>::new(),
3383        );
3384        let discovered = learn_peer_capabilities(&[oversized_tag]);
3385        assert!(discovered.supports_oversized_transfer);
3386
3387        // Disabled server: client flag must be ignored.
3388        let mut session = ClientSession::new(false);
3389        let oversized_enabled = false;
3390        session.supports_oversized_transfer |=
3391            oversized_enabled && discovered.supports_oversized_transfer;
3392        assert!(!session.supports_oversized_transfer);
3393
3394        // Enabled server: client flag is learned.
3395        let oversized_enabled = true;
3396        session.supports_oversized_transfer |=
3397            oversized_enabled && discovered.supports_oversized_transfer;
3398        assert!(session.supports_oversized_transfer);
3399    }
3400
3401    // ── CEP-41 open-stream capability advertisement ─────────────
3402
3403    #[test]
3404    fn test_open_stream_support_tags_helper() {
3405        // Disabled (the default) → no tag; enabled → the single-element tag.
3406        assert!(open_stream_support_tags(&OpenStreamConfig::default()).is_empty());
3407        let names = first_tag_values(&open_stream_support_tags(&OpenStreamConfig::enabled()));
3408        assert_eq!(names, vec!["support_open_stream"]);
3409    }
3410
3411    #[test]
3412    fn test_internal_common_capability_tags_merges_both() {
3413        let config = NostrServerTransportConfig::default()
3414            .with_oversized_enabled(true)
3415            .with_open_stream(OpenStreamConfig::enabled());
3416        let names = first_tag_values(&internal_common_capability_tags(&config));
3417        assert!(names.contains(&"support_oversized_transfer".to_string()));
3418        assert!(names.contains(&"support_open_stream".to_string()));
3419    }
3420
3421    #[tokio::test]
3422    async fn test_announcement_includes_open_stream_tag_when_enabled() {
3423        let config = NostrServerTransportConfig {
3424            open_stream: OpenStreamConfig::enabled(),
3425            ..Default::default()
3426        };
3427        let pool: Arc<dyn RelayPoolTrait> = Arc::new(crate::relay::mock::MockRelayPool::new());
3428        let server = NostrServerTransport::with_relay_pool(config, pool)
3429            .await
3430            .expect("server transport construction");
3431        let names = first_tag_values(&server.announcement_manager.get_common_tags());
3432        assert!(
3433            names.contains(&"support_open_stream".to_string()),
3434            "announcement must advertise open-stream support when enabled"
3435        );
3436    }
3437
3438    #[tokio::test]
3439    async fn test_announcement_omits_open_stream_tag_when_disabled() {
3440        // The default config has open-stream disabled (opt-in).
3441        let pool: Arc<dyn RelayPoolTrait> = Arc::new(crate::relay::mock::MockRelayPool::new());
3442        let server =
3443            NostrServerTransport::with_relay_pool(NostrServerTransportConfig::default(), pool)
3444                .await
3445                .expect("server transport construction");
3446        let names = first_tag_values(&server.announcement_manager.get_common_tags());
3447        assert!(!names.contains(&"support_open_stream".to_string()));
3448    }
3449
3450    #[test]
3451    fn test_server_learns_client_open_stream_only_when_enabled() {
3452        let open_stream_tag = Tag::custom(
3453            TagKind::Custom(tags::SUPPORT_OPEN_STREAM.into()),
3454            Vec::<String>::new(),
3455        );
3456        let discovered = learn_peer_capabilities(&[open_stream_tag]);
3457        assert!(discovered.supports_open_stream);
3458
3459        // Disabled server: the client flag is ignored.
3460        let mut session = ClientSession::new(false);
3461        let open_stream_enabled = false;
3462        session.supports_open_stream |= open_stream_enabled && discovered.supports_open_stream;
3463        assert!(!session.supports_open_stream);
3464
3465        // Enabled server: the client flag is learned.
3466        let open_stream_enabled = true;
3467        session.supports_open_stream |= open_stream_enabled && discovered.supports_open_stream;
3468        assert!(session.supports_open_stream);
3469    }
3470
3471    // ── CEP-41 response deferral (try_defer_open_stream_response) ───────
3472
3473    /// A no-op writer (publishes nothing) for exercising the deferral decision.
3474    fn deferral_test_writer(token: &str) -> OpenStreamWriter {
3475        let publish_frame: PublishFrame = Arc::new(|_frame: JsonRpcNotification| {
3476            Box::pin(async move { Ok(EventId::all_zeros()) })
3477        });
3478        OpenStreamWriter::new(OpenStreamWriterOptions {
3479            progress_token: token.to_string(),
3480            publish_frame,
3481            content_type: None,
3482            on_close: None,
3483            on_abort: None,
3484        })
3485    }
3486
3487    /// Install a writer slot + `token → event_id` index entry, mirroring
3488    /// `create_open_stream_writer`.
3489    fn install_slot(
3490        state: &ServerOpenStreamState,
3491        event_id: &str,
3492        writer: OpenStreamWriter,
3493        terminated: bool,
3494    ) {
3495        let token = writer.progress_token().to_string();
3496        let snapshot = RouteSnapshot {
3497            client_pubkey: Keys::generate().public_key(),
3498            original_request_id: serde_json::json!(1),
3499            is_encrypted: false,
3500            mirrored_wrap_kind: None,
3501        };
3502        state.lock_slots().insert(
3503            event_id.to_string(),
3504            OpenStreamSlot {
3505                writer,
3506                snapshot,
3507                pending_response: None,
3508                terminated,
3509            },
3510        );
3511        state.lock_token_index().insert(token, event_id.to_string());
3512    }
3513
3514    fn dummy_response() -> JsonRpcMessage {
3515        JsonRpcMessage::Response(JsonRpcResponse {
3516            jsonrpc: "2.0".to_string(),
3517            id: serde_json::json!(1),
3518            result: serde_json::json!({ "ok": true }),
3519        })
3520    }
3521
3522    #[tokio::test]
3523    async fn try_defer_open_stream_response_branch_coverage() {
3524        let config = NostrServerTransportConfig::default()
3525            .with_open_stream(OpenStreamConfig::default().with_enabled(true));
3526        let pool: Arc<dyn RelayPoolTrait> = Arc::new(MockRelayPool::new());
3527        let transport = NostrServerTransport::with_relay_pool(config, pool)
3528            .await
3529            .expect("server transport");
3530
3531        // No slot for the event (`slots.get_mut` is None) → Passthrough.
3532        assert!(matches!(
3533            transport.try_defer_open_stream_response("evt-none", dummy_response()),
3534            OpenStreamDeferral::Passthrough(_)
3535        ));
3536
3537        // `!writer.has_started()` — writer created (progressToken present) but the
3538        // tool never streamed → drop the writer and Passthrough. The slot AND the
3539        // token index entry must both be removed so the unused writer cannot leak.
3540        install_slot(
3541            &transport.open_stream,
3542            "evt-unstarted",
3543            deferral_test_writer("tok-unstarted"),
3544            false,
3545        );
3546        assert!(matches!(
3547            transport.try_defer_open_stream_response("evt-unstarted", dummy_response()),
3548            OpenStreamDeferral::Passthrough(_)
3549        ));
3550        assert!(
3551            transport
3552                .open_stream
3553                .lock_slots()
3554                .get("evt-unstarted")
3555                .is_none(),
3556            "unstarted writer slot must be removed (no leak)"
3557        );
3558        assert!(
3559            transport
3560                .open_stream
3561                .lock_token_index()
3562                .get("tok-unstarted")
3563                .is_none(),
3564            "unstarted writer token index must be removed (no leak)"
3565        );
3566
3567        // `slot.terminated` (the function's "Ordering B") — started writer whose
3568        // stream already closed/aborted → deliver now from the snapshot (SendNow);
3569        // the slot + token index are freed.
3570        let terminal = deferral_test_writer("tok-terminal");
3571        terminal.start().await.expect("start");
3572        install_slot(&transport.open_stream, "evt-terminal", terminal, true);
3573        assert!(matches!(
3574            transport.try_defer_open_stream_response("evt-terminal", dummy_response()),
3575            OpenStreamDeferral::SendNow { .. }
3576        ));
3577        assert!(transport
3578            .open_stream
3579            .lock_slots()
3580            .get("evt-terminal")
3581            .is_none());
3582        assert!(transport
3583            .open_stream
3584            .lock_token_index()
3585            .get("tok-terminal")
3586            .is_none());
3587
3588        // `else` of `slot.terminated` (the function's "Ordering A") — started
3589        // writer, stream still open → Deferred. The response is stashed and the
3590        // slot retained for the close/abort hook to flush.
3591        let open = deferral_test_writer("tok-open");
3592        open.start().await.expect("start");
3593        install_slot(&transport.open_stream, "evt-open", open, false);
3594        assert!(matches!(
3595            transport.try_defer_open_stream_response("evt-open", dummy_response()),
3596            OpenStreamDeferral::Deferred
3597        ));
3598        {
3599            let slots = transport.open_stream.lock_slots();
3600            let slot = slots.get("evt-open").expect("deferred slot retained");
3601            assert!(
3602                slot.pending_response.is_some(),
3603                "the deferred response must be stashed for the hook to flush"
3604            );
3605        }
3606
3607        // Disabled gate — a server with open-stream disabled never exposes a
3608        // writer, so `send_response` never reaches the deferral decision at all.
3609        let disabled = NostrServerTransport::with_relay_pool(
3610            NostrServerTransportConfig::default()
3611                .with_open_stream(OpenStreamConfig::default().with_enabled(false)),
3612            Arc::new(MockRelayPool::new()) as Arc<dyn RelayPoolTrait>,
3613        )
3614        .await
3615        .expect("disabled server transport");
3616        install_slot(
3617            &disabled.open_stream,
3618            "evt-disabled",
3619            deferral_test_writer("tok-disabled"),
3620            false,
3621        );
3622        assert!(
3623            disabled.get_open_stream_writer("evt-disabled").is_none(),
3624            "a disabled server must not expose writers (deferral never attempted)"
3625        );
3626    }
3627}