Skip to main content

arete_server/websocket/
client_manager.rs

1use crate::account_policy::{redact_identity, AccountPolicyError, AccountPolicyRegistry};
2use crate::compression::CompressedPayload;
3use crate::websocket::auth::{AuthContext, AuthDeny, AuthErrorCode};
4use crate::websocket::rate_limiter::{RateLimitResult, WebSocketRateLimiter};
5use arete_auth::Limits;
6use bytes::Bytes;
7use dashmap::DashMap;
8use futures_util::stream::SplitSink;
9use futures_util::SinkExt;
10use std::collections::{HashMap, HashSet};
11use std::net::SocketAddr;
12use std::sync::Arc;
13use std::time::{Duration, Instant, SystemTime};
14use tokio::net::TcpStream;
15use tokio::sync::{mpsc, RwLock};
16use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
17use tokio_tungstenite::tungstenite::protocol::CloseFrame;
18use tokio_tungstenite::{tungstenite::Message, WebSocketStream};
19use tokio_util::sync::CancellationToken;
20use tracing::{debug, info, warn};
21use uuid::Uuid;
22
23pub type WebSocketSender = SplitSink<WebSocketStream<TcpStream>, Message>;
24
25/// Error type for send operations
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum SendError {
28    /// Client not found in registry
29    ClientNotFound,
30    /// Client's message queue is full - client was disconnected
31    ClientBackpressured,
32    /// Client's channel is closed - client was disconnected
33    ClientDisconnected,
34}
35
36impl std::fmt::Display for SendError {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            SendError::ClientNotFound => write!(f, "client not found"),
40            SendError::ClientBackpressured => write!(f, "client backpressured and disconnected"),
41            SendError::ClientDisconnected => write!(f, "client disconnected"),
42        }
43    }
44}
45
46impl std::error::Error for SendError {}
47
48/// Egress tracking for a client
49#[derive(Debug)]
50struct EgressTracker {
51    /// Bytes sent in the current minute window
52    bytes_this_minute: u64,
53    /// Start of the current minute window
54    window_start: SystemTime,
55}
56
57/// Inbound message-rate tracking for a client
58#[derive(Debug)]
59struct MessageRateTracker {
60    messages_this_minute: u32,
61    window_start: SystemTime,
62}
63
64impl MessageRateTracker {
65    fn new() -> Self {
66        Self {
67            messages_this_minute: 0,
68            window_start: SystemTime::now(),
69        }
70    }
71
72    fn maybe_reset_window(&mut self) {
73        let now = SystemTime::now();
74        if now.duration_since(self.window_start).unwrap_or_default() >= Duration::from_secs(60) {
75            self.messages_this_minute = 0;
76            self.window_start = now;
77        }
78    }
79
80    fn record_message(&mut self, limit: u32) -> bool {
81        self.maybe_reset_window();
82        if self.messages_this_minute + 1 > limit {
83            false
84        } else {
85            self.messages_this_minute += 1;
86            true
87        }
88    }
89
90    fn current_usage(&mut self) -> u32 {
91        self.maybe_reset_window();
92        self.messages_this_minute
93    }
94}
95
96impl EgressTracker {
97    fn new() -> Self {
98        Self {
99            bytes_this_minute: 0,
100            window_start: SystemTime::now(),
101        }
102    }
103
104    /// Check if we need to reset the window (new minute)
105    fn maybe_reset_window(&mut self) {
106        let now = SystemTime::now();
107        if now.duration_since(self.window_start).unwrap_or_default() >= Duration::from_secs(60) {
108            self.bytes_this_minute = 0;
109            self.window_start = now;
110        }
111    }
112
113    /// Record bytes sent, returning true if within limit
114    fn record_bytes(&mut self, bytes: usize, limit: u64) -> bool {
115        self.maybe_reset_window();
116        let bytes_u64 = bytes as u64;
117        if self.bytes_this_minute + bytes_u64 > limit {
118            false
119        } else {
120            self.bytes_this_minute += bytes_u64;
121            true
122        }
123    }
124
125    /// Get current usage
126    fn current_usage(&mut self) -> u64 {
127        self.maybe_reset_window();
128        self.bytes_this_minute
129    }
130}
131
132/// Aggregate message/egress usage shared by every connection of one account.
133///
134/// This is a per-process tracker, not a globally exact quota; durable
135/// cross-replica metering is a separate system.
136#[derive(Debug)]
137struct AccountUsage {
138    egress: std::sync::Mutex<EgressTracker>,
139    messages: std::sync::Mutex<MessageRateTracker>,
140    last_seen: std::sync::Mutex<Instant>,
141}
142
143impl AccountUsage {
144    fn new() -> Self {
145        Self {
146            egress: std::sync::Mutex::new(EgressTracker::new()),
147            messages: std::sync::Mutex::new(MessageRateTracker::new()),
148            last_seen: std::sync::Mutex::new(Instant::now()),
149        }
150    }
151
152    fn touch(&self) {
153        if let Ok(mut last_seen) = self.last_seen.lock() {
154            *last_seen = Instant::now();
155        }
156    }
157
158    fn record_egress(&self, bytes: usize, limit: u64) -> bool {
159        self.touch();
160        match self.egress.lock() {
161            Ok(mut tracker) => tracker.record_bytes(bytes, limit),
162            Err(_) => true,
163        }
164    }
165
166    fn record_message(&self, limit: u32) -> bool {
167        self.touch();
168        match self.messages.lock() {
169            Ok(mut tracker) => tracker.record_message(limit),
170            Err(_) => true,
171        }
172    }
173
174    fn idle_for(&self) -> Duration {
175        self.last_seen
176            .lock()
177            .map(|last_seen| last_seen.elapsed())
178            .unwrap_or(Duration::MAX)
179    }
180}
181
182/// Information about a connected client
183#[derive(Debug)]
184pub struct ClientInfo {
185    pub id: Uuid,
186    pub last_seen: SystemTime,
187    pub sender: mpsc::Sender<Message>,
188    subscriptions: Arc<RwLock<HashMap<String, CancellationToken>>>,
189    /// Authentication context for this client
190    pub auth_context: Option<AuthContext>,
191    /// Client's IP address for rate limiting
192    pub remote_addr: SocketAddr,
193    /// Egress tracking for rate limiting
194    egress_tracker: std::sync::Mutex<EgressTracker>,
195    /// Inbound message-rate tracking for rate limiting
196    message_rate_tracker: std::sync::Mutex<MessageRateTracker>,
197    /// Why the server dropped this client, sent as the close frame once the
198    /// queue drains. It is kept out of the queue so that a full queue cannot
199    /// lose it.
200    close_frame: Arc<std::sync::OnceLock<CloseFrame>>,
201}
202
203impl ClientInfo {
204    pub fn new(
205        id: Uuid,
206        sender: mpsc::Sender<Message>,
207        auth_context: Option<AuthContext>,
208        remote_addr: SocketAddr,
209    ) -> Self {
210        Self {
211            id,
212            last_seen: SystemTime::now(),
213            sender,
214            subscriptions: Arc::new(RwLock::new(HashMap::new())),
215            auth_context,
216            remote_addr,
217            egress_tracker: std::sync::Mutex::new(EgressTracker::new()),
218            message_rate_tracker: std::sync::Mutex::new(MessageRateTracker::new()),
219            close_frame: Arc::new(std::sync::OnceLock::new()),
220        }
221    }
222
223    /// Record bytes sent, returning true if within limit
224    pub fn record_egress(&self, bytes: usize) -> Option<u64> {
225        if let Ok(mut tracker) = self.egress_tracker.lock() {
226            if let Some(ref ctx) = self.auth_context {
227                if let Some(limit) = ctx.limits.max_bytes_per_minute {
228                    if tracker.record_bytes(bytes, limit) {
229                        return Some(tracker.current_usage());
230                    } else {
231                        return None; // Limit exceeded
232                    }
233                }
234            }
235            // No limit set, return current usage (0)
236            return Some(tracker.current_usage());
237        }
238        None
239    }
240
241    /// Record an inbound client message, returning true if within limit.
242    pub fn record_inbound_message(&self) -> Option<u32> {
243        if let Ok(mut tracker) = self.message_rate_tracker.lock() {
244            if let Some(ref ctx) = self.auth_context {
245                if let Some(limit) = ctx.limits.max_messages_per_minute {
246                    if tracker.record_message(limit) {
247                        return Some(tracker.current_usage());
248                    } else {
249                        return None;
250                    }
251                }
252            }
253
254            return Some(tracker.current_usage());
255        }
256
257        None
258    }
259
260    pub fn update_last_seen(&mut self) {
261        self.last_seen = SystemTime::now();
262    }
263
264    pub fn is_stale(&self, timeout: Duration) -> bool {
265        self.last_seen.elapsed().unwrap_or(Duration::MAX) > timeout
266    }
267
268    pub async fn add_subscription(
269        &self,
270        subscription_id: String,
271        token: CancellationToken,
272    ) -> bool {
273        let mut subs = self.subscriptions.write().await;
274        match subs.entry(subscription_id) {
275            std::collections::hash_map::Entry::Vacant(entry) => {
276                entry.insert(token);
277                true
278            }
279            std::collections::hash_map::Entry::Occupied(_) => false,
280        }
281    }
282
283    pub async fn remove_subscription(&self, subscription_id: &str) -> bool {
284        let mut subs = self.subscriptions.write().await;
285        if let Some(token) = subs.remove(subscription_id) {
286            token.cancel();
287            debug!("Cancelled subscription: {}", subscription_id);
288            true
289        } else {
290            debug!(
291                "Subscription not found for cancellation: {}",
292                subscription_id
293            );
294            false
295        }
296    }
297
298    pub async fn cancel_all_subscriptions(&self) {
299        let subs = self.subscriptions.read().await;
300        for (subscription_id, token) in subs.iter() {
301            token.cancel();
302            debug!("Cancelled subscription on disconnect: {}", subscription_id);
303        }
304    }
305
306    pub async fn subscription_count(&self) -> usize {
307        self.subscriptions.read().await.len()
308    }
309}
310
311/// Configuration for rate limiting in ClientManager
312///
313/// These settings control various rate limits at the connection level.
314/// Per-subject limits are controlled via AuthContext.Limits.
315#[derive(Debug, Clone)]
316pub struct RateLimitConfig {
317    /// Global maximum connections per IP address
318    pub max_connections_per_ip: Option<usize>,
319    /// Global maximum connections per metering key
320    pub max_connections_per_metering_key: Option<usize>,
321    /// Global maximum connections per origin
322    pub max_connections_per_origin: Option<usize>,
323    /// Default connection timeout for stale client cleanup
324    pub client_timeout: Duration,
325    /// Message queue size per client
326    pub message_queue_size: usize,
327    /// Maximum reconnect attempts per client (optional global default)
328    pub max_reconnect_attempts: Option<u32>,
329    /// Rate limit window duration for message counting
330    pub message_rate_window: Duration,
331    /// Rate limit window duration for egress tracking
332    pub egress_rate_window: Duration,
333    /// Default limits applied when auth token doesn't specify limits
334    /// These act as server-wide fallback limits for all connections
335    pub default_limits: Option<Limits>,
336}
337
338impl Default for RateLimitConfig {
339    fn default() -> Self {
340        Self {
341            max_connections_per_ip: None,
342            max_connections_per_metering_key: None,
343            max_connections_per_origin: None,
344            client_timeout: Duration::from_secs(300),
345            message_queue_size: 512,
346            max_reconnect_attempts: None,
347            message_rate_window: Duration::from_secs(60),
348            egress_rate_window: Duration::from_secs(60),
349            default_limits: None,
350        }
351    }
352}
353
354impl RateLimitConfig {
355    /// Load configuration from environment variables
356    ///
357    /// Environment variables:
358    /// - `ARETE_WS_MAX_CONNECTIONS_PER_IP` - Max connections per IP (default: unlimited)
359    /// - `ARETE_WS_MAX_CONNECTIONS_PER_METERING_KEY` - Max connections per metering key (default: unlimited)
360    /// - `ARETE_WS_MAX_CONNECTIONS_PER_ORIGIN` - Max connections per origin (default: unlimited)
361    /// - `ARETE_WS_CLIENT_TIMEOUT_SECS` - Client timeout in seconds (default: 300)
362    /// - `ARETE_WS_MESSAGE_QUEUE_SIZE` - Message queue size per client (default: 512)
363    /// - `ARETE_WS_RATE_LIMIT_WINDOW_SECS` - Rate limit window in seconds (default: 60)
364    /// - `ARETE_WS_DEFAULT_MAX_CONNECTIONS` - Default max connections per subject (fallback when token has no limit)
365    /// - `ARETE_WS_DEFAULT_MAX_SUBSCRIPTIONS` - Default max subscriptions per connection (fallback when token has no limit)
366    /// - `ARETE_WS_DEFAULT_MAX_SNAPSHOT_ROWS` - Default max snapshot rows per request (fallback when token has no limit)
367    /// - `ARETE_WS_DEFAULT_MAX_MESSAGES_PER_MINUTE` - Default max messages per minute (fallback when token has no limit)
368    /// - `ARETE_WS_DEFAULT_MAX_BYTES_PER_MINUTE` - Default max bytes per minute (fallback when token has no limit)
369    pub fn from_env() -> Self {
370        let mut config = Self::default();
371
372        if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_IP") {
373            if let Ok(max) = val.parse() {
374                config.max_connections_per_ip = Some(max);
375            }
376        }
377
378        if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_METERING_KEY") {
379            if let Ok(max) = val.parse() {
380                config.max_connections_per_metering_key = Some(max);
381            }
382        }
383
384        if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_ORIGIN") {
385            if let Ok(max) = val.parse() {
386                config.max_connections_per_origin = Some(max);
387            }
388        }
389
390        if let Ok(val) = std::env::var("ARETE_WS_CLIENT_TIMEOUT_SECS") {
391            if let Ok(secs) = val.parse() {
392                config.client_timeout = Duration::from_secs(secs);
393            }
394        }
395
396        if let Ok(val) = std::env::var("ARETE_WS_MESSAGE_QUEUE_SIZE") {
397            if let Ok(size) = val.parse() {
398                config.message_queue_size = size;
399            }
400        }
401
402        if let Ok(val) = std::env::var("ARETE_WS_RATE_LIMIT_WINDOW_SECS") {
403            if let Ok(secs) = val.parse() {
404                config.message_rate_window = Duration::from_secs(secs);
405                config.egress_rate_window = Duration::from_secs(secs);
406            }
407        }
408
409        // Load default limits from environment (fallback when auth token doesn't specify limits)
410        let mut default_limits = Limits::default();
411        let mut has_default_limits = false;
412
413        if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_CONNECTIONS") {
414            if let Ok(max) = val.parse() {
415                default_limits.max_connections = Some(max);
416                has_default_limits = true;
417            }
418        }
419
420        if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_SUBSCRIPTIONS") {
421            if let Ok(max) = val.parse() {
422                default_limits.max_subscriptions = Some(max);
423                has_default_limits = true;
424            }
425        }
426
427        if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_SNAPSHOT_ROWS") {
428            if let Ok(max) = val.parse() {
429                default_limits.max_snapshot_rows = Some(max);
430                has_default_limits = true;
431            }
432        }
433
434        if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_MESSAGES_PER_MINUTE") {
435            if let Ok(max) = val.parse() {
436                default_limits.max_messages_per_minute = Some(max);
437                has_default_limits = true;
438            }
439        }
440
441        if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_BYTES_PER_MINUTE") {
442            if let Ok(max) = val.parse() {
443                default_limits.max_bytes_per_minute = Some(max);
444                has_default_limits = true;
445            }
446        }
447
448        if has_default_limits {
449            config.default_limits = Some(default_limits);
450        }
451
452        config
453    }
454
455    /// Set the maximum connections per IP
456    pub fn with_max_connections_per_ip(mut self, max: usize) -> Self {
457        self.max_connections_per_ip = Some(max);
458        self
459    }
460
461    /// Set the client timeout
462    pub fn with_timeout(mut self, timeout: Duration) -> Self {
463        self.client_timeout = timeout;
464        self
465    }
466
467    /// Set the message queue size
468    pub fn with_message_queue_size(mut self, size: usize) -> Self {
469        self.message_queue_size = size;
470        self
471    }
472
473    /// Set the rate limit window (applies to both message and egress windows)
474    pub fn with_rate_limit_window(mut self, window: Duration) -> Self {
475        self.message_rate_window = window;
476        self.egress_rate_window = window;
477        self
478    }
479
480    /// Set default limits applied when auth token doesn't specify limits
481    ///
482    /// These limits act as server-wide fallbacks for connections
483    /// where the authentication token doesn't include explicit limits.
484    pub fn with_default_limits(mut self, limits: Limits) -> Self {
485        self.default_limits = Some(limits);
486        self
487    }
488}
489
490/// Bound on tracked account usage entries; signed account keys keep the
491/// cardinality legitimate, this is a hard backstop.
492const MAX_TRACKED_ACCOUNT_USAGE: usize = crate::account_policy::DEFAULT_MAX_TRACKED_ACCOUNTS;
493
494/// Idle TTL after which aggregate account usage state is evicted.
495const ACCOUNT_USAGE_IDLE_TTL: Duration = crate::account_policy::DEFAULT_ACCOUNT_IDLE_TTL;
496
497/// Manages all connected WebSocket clients using lock-free DashMap.
498///
499/// Key design decisions:
500/// - Uses DashMap for lock-free concurrent access to client registry
501/// - Uses try_send instead of send to never block on slow clients
502/// - Disconnects clients that are backpressured (queue full) to prevent cascade failures
503/// - All public methods are non-blocking or use fine-grained per-key locks
504/// - Supports configurable rate limiting per IP, subject, and global defaults
505#[derive(Clone)]
506pub struct ClientManager {
507    clients: Arc<DashMap<Uuid, ClientInfo>>,
508    rate_limit_config: RateLimitConfig,
509    /// Optional WebSocket rate limiter for granular rate control
510    rate_limiter: Option<Arc<WebSocketRateLimiter>>,
511    /// Per-process account policy versions and signed aggregate limits
512    account_policies: Arc<AccountPolicyRegistry>,
513    /// Per-process aggregate message/egress usage per account
514    account_usage: Arc<DashMap<String, AccountUsage>>,
515}
516
517impl ClientManager {
518    pub fn new() -> Self {
519        Self::with_config(RateLimitConfig::default())
520    }
521
522    /// Create a new ClientManager with the given rate limit configuration
523    pub fn with_config(config: RateLimitConfig) -> Self {
524        Self {
525            clients: Arc::new(DashMap::new()),
526            rate_limit_config: config,
527            rate_limiter: None,
528            account_policies: Arc::new(AccountPolicyRegistry::default()),
529            account_usage: Arc::new(DashMap::new()),
530        }
531    }
532
533    /// Load configuration from environment variables
534    ///
535    /// See `RateLimitConfig::from_env` for supported variables.
536    pub fn from_env() -> Self {
537        Self::with_config(RateLimitConfig::from_env())
538    }
539
540    /// Set the client timeout for stale client cleanup
541    pub fn with_timeout(mut self, timeout: Duration) -> Self {
542        self.rate_limit_config.client_timeout = timeout;
543        self
544    }
545
546    /// Set the message queue size per client
547    pub fn with_message_queue_size(mut self, queue_size: usize) -> Self {
548        self.rate_limit_config.message_queue_size = queue_size;
549        self
550    }
551
552    /// Set a global limit on connections per IP address
553    pub fn with_max_connections_per_ip(mut self, max: usize) -> Self {
554        self.rate_limit_config.max_connections_per_ip = Some(max);
555        self
556    }
557
558    /// Set the rate limit window duration
559    pub fn with_rate_limit_window(mut self, window: Duration) -> Self {
560        self.rate_limit_config.message_rate_window = window;
561        self.rate_limit_config.egress_rate_window = window;
562        self
563    }
564
565    /// Set default limits applied when auth token doesn't specify limits
566    ///
567    /// These limits act as server-wide fallbacks for connections
568    /// where the authentication token doesn't include explicit limits.
569    pub fn with_default_limits(mut self, limits: Limits) -> Self {
570        self.rate_limit_config.default_limits = Some(limits);
571        self
572    }
573
574    /// Set a WebSocket rate limiter for granular rate control
575    pub fn with_rate_limiter(mut self, rate_limiter: Arc<WebSocketRateLimiter>) -> Self {
576        self.rate_limiter = Some(rate_limiter);
577        self
578    }
579
580    /// Get the rate limiter if configured
581    pub fn rate_limiter(&self) -> Option<&WebSocketRateLimiter> {
582        self.rate_limiter.as_ref().map(|r| r.as_ref())
583    }
584
585    /// Get the current rate limit configuration
586    pub fn rate_limit_config(&self) -> &RateLimitConfig {
587        &self.rate_limit_config
588    }
589
590    /// Account policy state observed by this manager.
591    pub fn account_policies(&self) -> &AccountPolicyRegistry {
592        &self.account_policies
593    }
594
595    /// Record aggregate account egress; true when within the signed limit.
596    fn record_account_egress(&self, ctx: &AuthContext, bytes: usize) -> bool {
597        if ctx.is_legacy_policy() || ctx.account_key.is_none() {
598            return true;
599        }
600        let Some(limit) = ctx.account_limits.max_bytes_per_minute else {
601            return true;
602        };
603        let account = ctx.account_key();
604        if let Some(usage) = self.account_usage.get(account) {
605            return usage.record_egress(bytes, limit);
606        }
607        if self.account_usage.len() >= MAX_TRACKED_ACCOUNT_USAGE {
608            warn!(
609                account = %redact_identity(account),
610                "account usage tracking at capacity; aggregate egress guard skipped"
611            );
612            return true;
613        }
614        self.account_usage
615            .entry(account.to_string())
616            .or_insert_with(AccountUsage::new)
617            .record_egress(bytes, limit)
618    }
619
620    /// Record one aggregate account inbound message; true when within limit.
621    fn record_account_message(&self, ctx: &AuthContext) -> bool {
622        if ctx.is_legacy_policy() || ctx.account_key.is_none() {
623            return true;
624        }
625        let Some(limit) = ctx.account_limits.max_messages_per_minute else {
626            return true;
627        };
628        let account = ctx.account_key();
629        if let Some(usage) = self.account_usage.get(account) {
630            return usage.record_message(limit);
631        }
632        if self.account_usage.len() >= MAX_TRACKED_ACCOUNT_USAGE {
633            warn!(
634                account = %redact_identity(account),
635                "account usage tracking at capacity; aggregate message guard skipped"
636            );
637            return true;
638        }
639        self.account_usage
640            .entry(account.to_string())
641            .or_insert_with(AccountUsage::new)
642            .record_message(limit)
643    }
644
645    /// Enforce the per-connection and aggregate account egress budgets.
646    fn enforce_egress_budgets(&self, client_id: Uuid, bytes: usize) -> Result<(), SendError> {
647        let (connection_ok, account_ok) = {
648            let Some(client) = self.clients.get(&client_id) else {
649                return Err(SendError::ClientNotFound);
650            };
651            let connection_ok = client.record_egress(bytes).is_some();
652            let account_ok = !connection_ok
653                || client
654                    .auth_context
655                    .as_ref()
656                    .map(|ctx| self.record_account_egress(ctx, bytes))
657                    .unwrap_or(true);
658            (connection_ok, account_ok)
659        };
660        if !connection_ok {
661            warn!("Client {} exceeded egress limit, disconnecting", client_id);
662            self.clients.remove(&client_id);
663            return Err(SendError::ClientDisconnected);
664        }
665        if !account_ok {
666            warn!(
667                "Client {} exceeded the account egress limit, disconnecting",
668                client_id
669            );
670            self.clients.remove(&client_id);
671            return Err(SendError::ClientDisconnected);
672        }
673        Ok(())
674    }
675
676    /// Add a new client connection.
677    ///
678    /// Spawns a dedicated sender task for this client that reads from its mpsc channel
679    /// and writes to the WebSocket. If the WebSocket write fails, the client is automatically
680    /// removed from the registry.
681    pub fn add_client(
682        &self,
683        client_id: Uuid,
684        mut ws_sender: WebSocketSender,
685        auth_context: Option<AuthContext>,
686        remote_addr: SocketAddr,
687    ) {
688        let (client_tx, mut client_rx) =
689            mpsc::channel::<Message>(self.rate_limit_config.message_queue_size);
690        let client_info = ClientInfo::new(client_id, client_tx, auth_context, remote_addr);
691        let close_frame = client_info.close_frame.clone();
692
693        let clients_ref = self.clients.clone();
694        tokio::spawn(async move {
695            while let Some(message) = client_rx.recv().await {
696                if let Err(e) = ws_sender.send(message).await {
697                    warn!("Failed to send message to client {}: {}", client_id, e);
698                    break;
699                }
700            }
701            // Removing a client closes its queue. Complete the WebSocket close
702            // handshake as well so receivers do not wait forever on a socket
703            // whose server-side delivery task has already stopped, and say why
704            // when the server dropped the client on purpose.
705            if let Some(frame) = close_frame.get() {
706                let _ = ws_sender.send(Message::Close(Some(frame.clone()))).await;
707            }
708            let _ = ws_sender.close().await;
709            clients_ref.remove(&client_id);
710            debug!("WebSocket sender task for client {} stopped", client_id);
711        });
712
713        self.clients.insert(client_id, client_info);
714        info!("Client {} registered from {}", client_id, remote_addr);
715    }
716
717    /// Remove a client from the registry.
718    pub fn remove_client(&self, client_id: Uuid) {
719        if self.clients.remove(&client_id).is_some() {
720            info!("Client {} removed", client_id);
721        }
722    }
723
724    /// Update the auth context for a client.
725    ///
726    /// Used for in-band auth refresh without reconnecting.
727    pub fn update_client_auth(&self, client_id: Uuid, auth_context: AuthContext) -> bool {
728        if let Some(mut client) = self.clients.get_mut(&client_id) {
729            client.auth_context = Some(auth_context);
730            debug!("Updated auth context for client {}", client_id);
731            true
732        } else {
733            false
734        }
735    }
736
737    /// Check if a client's token has expired.
738    ///
739    /// Returns true if the client has an auth context and it has expired.
740    /// If expired, the client is removed from the registry.
741    pub fn check_and_remove_expired(&self, client_id: Uuid) -> bool {
742        let now = std::time::SystemTime::now()
743            .duration_since(std::time::UNIX_EPOCH)
744            .unwrap_or_default()
745            .as_secs();
746        // Check and remove under one shard lock. Calling `remove` while a
747        // `get` guard on the same key is alive deadlocks this thread, and
748        // every later caller that touches the shard queues behind it.
749        let Some((_, client)) = self.clients.remove_if(&client_id, |_, client| {
750            client
751                .auth_context
752                .as_ref()
753                .is_some_and(|ctx| ctx.expires_at <= now)
754        }) else {
755            return false;
756        };
757        if let Some(ctx) = &client.auth_context {
758            warn!(
759                "Client {} token expired (expired at {}), disconnecting",
760                client_id, ctx.expires_at
761            );
762        }
763        // Say why the socket closes. The sender task sends this after the
764        // queue drains, so it is the last frame the client sees; the SDKs read
765        // a `token-expired:` reason as "mint a new token and reconnect".
766        let _ = client.close_frame.set(CloseFrame {
767            code: CloseCode::Policy,
768            reason: format!(
769                "{}: Authentication token expired",
770                AuthErrorCode::TokenExpired.as_str()
771            )
772            .into(),
773        });
774        true
775    }
776
777    /// Get the current number of connected clients.
778    ///
779    /// This is lock-free and returns an approximate count (may be slightly stale
780    /// under high concurrency, which is fine for max_clients checks).
781    pub fn client_count(&self) -> usize {
782        self.clients.len()
783    }
784
785    /// Send data to a specific client (non-blocking).
786    ///
787    /// This method NEVER blocks. If the client's queue is full, the client is
788    /// considered too slow and is disconnected to prevent cascade failures.
789    /// Use this for live streaming updates.
790    ///
791    /// For initial snapshots where you expect to send many messages at once,
792    /// use `send_to_client_async` instead which will wait for queue space.
793    pub fn send_to_client(&self, client_id: Uuid, data: Arc<Bytes>) -> Result<(), SendError> {
794        // Check if client token has expired before sending
795        if self.check_and_remove_expired(client_id) {
796            return Err(SendError::ClientDisconnected);
797        }
798
799        // Check per-connection and aggregate account egress limits
800        self.enforce_egress_budgets(client_id, data.len())?;
801
802        let sender = {
803            let client = self
804                .clients
805                .get(&client_id)
806                .ok_or(SendError::ClientNotFound)?;
807            client.sender.clone()
808        };
809
810        let msg = Message::Binary((*data).clone());
811        match sender.try_send(msg) {
812            Ok(()) => Ok(()),
813            Err(mpsc::error::TrySendError::Full(_)) => {
814                warn!(
815                    "Client {} backpressured (queue full), disconnecting",
816                    client_id
817                );
818                self.clients.remove(&client_id);
819                Err(SendError::ClientBackpressured)
820            }
821            Err(mpsc::error::TrySendError::Closed(_)) => {
822                debug!("Client {} channel closed", client_id);
823                self.clients.remove(&client_id);
824                Err(SendError::ClientDisconnected)
825            }
826        }
827    }
828
829    /// Send data to a specific client (async, waits for queue space).
830    ///
831    /// This method will wait if the client's queue is full, allowing the client
832    /// time to catch up. Use this for initial snapshots where you need to send
833    /// many messages at once.
834    ///
835    /// For live streaming updates, use `send_to_client` instead which will
836    /// disconnect slow clients rather than blocking.
837    pub async fn send_to_client_async(
838        &self,
839        client_id: Uuid,
840        data: Arc<Bytes>,
841    ) -> Result<(), SendError> {
842        // Check if client token has expired before sending
843        if self.check_and_remove_expired(client_id) {
844            return Err(SendError::ClientDisconnected);
845        }
846
847        // Check per-connection and aggregate account egress limits
848        self.enforce_egress_budgets(client_id, data.len())?;
849
850        let sender = {
851            let client = self
852                .clients
853                .get(&client_id)
854                .ok_or(SendError::ClientNotFound)?;
855            client.sender.clone()
856        };
857
858        let msg = Message::Binary((*data).clone());
859        sender
860            .send(msg)
861            .await
862            .map_err(|_| SendError::ClientDisconnected)
863    }
864
865    /// Send a text message to a specific client (async).
866    ///
867    /// This method sends a text message directly to the client's WebSocket.
868    /// Used for control messages like auth refresh responses.
869    pub async fn send_text_to_client(
870        &self,
871        client_id: Uuid,
872        text: String,
873    ) -> Result<(), SendError> {
874        // Check if client token has expired before sending
875        if self.check_and_remove_expired(client_id) {
876            return Err(SendError::ClientDisconnected);
877        }
878
879        let sender = {
880            let client = self
881                .clients
882                .get(&client_id)
883                .ok_or(SendError::ClientNotFound)?;
884            client.sender.clone()
885        };
886
887        let msg = Message::Text(text.into());
888        sender
889            .send(msg)
890            .await
891            .map_err(|_| SendError::ClientDisconnected)
892    }
893
894    /// Send a potentially compressed payload to a client (async).
895    ///
896    /// Compressed payloads are sent as binary frames (raw gzip).
897    /// Uncompressed payloads are sent as text frames (JSON).
898    pub async fn send_compressed_async(
899        &self,
900        client_id: Uuid,
901        payload: CompressedPayload,
902    ) -> Result<(), SendError> {
903        // Check if client token has expired before sending
904        if self.check_and_remove_expired(client_id) {
905            return Err(SendError::ClientDisconnected);
906        }
907
908        let (sender, bytes_to_record) = {
909            let client = self
910                .clients
911                .get(&client_id)
912                .ok_or(SendError::ClientNotFound)?;
913
914            let bytes = match &payload {
915                CompressedPayload::Compressed(bytes) => bytes.len(),
916                CompressedPayload::Uncompressed(bytes) => bytes.len(),
917            };
918
919            (client.sender.clone(), bytes)
920        };
921
922        // Check per-connection and aggregate account egress limits
923        self.enforce_egress_budgets(client_id, bytes_to_record)?;
924
925        let msg = match payload {
926            CompressedPayload::Compressed(bytes) => Message::Binary(bytes),
927            CompressedPayload::Uncompressed(bytes) => Message::Binary(bytes),
928        };
929        sender
930            .send(msg)
931            .await
932            .map_err(|_| SendError::ClientDisconnected)
933    }
934
935    /// Update the last_seen timestamp for a client.
936    pub fn update_client_last_seen(&self, client_id: Uuid) {
937        if let Some(mut client) = self.clients.get_mut(&client_id) {
938            client.update_last_seen();
939        }
940    }
941
942    /// Check whether an inbound message is allowed for a client.
943    #[allow(clippy::result_large_err)]
944    pub fn check_inbound_message_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
945        if self.check_and_remove_expired(client_id) {
946            return Err(AuthDeny::new(
947                crate::websocket::auth::AuthErrorCode::TokenExpired,
948                "Authentication token expired",
949            ));
950        }
951
952        let (connection_ok, account_ok) = {
953            let Some(client) = self.clients.get(&client_id) else {
954                return Err(AuthDeny::new(
955                    crate::websocket::auth::AuthErrorCode::InternalError,
956                    "Client not found",
957                ));
958            };
959            let connection_ok = client.record_inbound_message().is_some();
960            let account_ok = !connection_ok
961                || client
962                    .auth_context
963                    .as_ref()
964                    .map(|ctx| self.record_account_message(ctx))
965                    .unwrap_or(true);
966            (connection_ok, account_ok)
967        };
968
969        if connection_ok && account_ok {
970            return Ok(());
971        }
972        self.clients.remove(&client_id);
973        let scope = if connection_ok {
974            "inbound account websocket messages"
975        } else {
976            "inbound websocket messages"
977        };
978        Err(
979            AuthDeny::rate_limited(self.rate_limit_config.message_rate_window, scope).with_context(
980                format!("client {} exceeded the inbound message budget", client_id),
981            ),
982        )
983    }
984
985    /// Check if a client exists.
986    pub fn has_client(&self, client_id: Uuid) -> bool {
987        self.clients.contains_key(&client_id)
988    }
989
990    pub async fn add_client_subscription(
991        &self,
992        client_id: Uuid,
993        subscription_id: String,
994        token: CancellationToken,
995    ) -> bool {
996        if let Some(client) = self.clients.get(&client_id) {
997            client.add_subscription(subscription_id, token).await
998        } else {
999            false
1000        }
1001    }
1002
1003    pub async fn remove_client_subscription(&self, client_id: Uuid, subscription_id: &str) -> bool {
1004        if let Some(client) = self.clients.get(&client_id) {
1005            client.remove_subscription(subscription_id).await
1006        } else {
1007            false
1008        }
1009    }
1010
1011    pub async fn cancel_all_client_subscriptions(&self, client_id: Uuid) {
1012        if let Some(client) = self.clients.get(&client_id) {
1013            client.cancel_all_subscriptions().await;
1014        }
1015    }
1016
1017    /// Remove stale clients that haven't been seen within the timeout period.
1018    pub fn cleanup_stale_clients(&self) -> usize {
1019        let timeout = self.rate_limit_config.client_timeout;
1020        let mut stale_clients = Vec::new();
1021
1022        for entry in self.clients.iter() {
1023            if entry.value().is_stale(timeout) {
1024                stale_clients.push(*entry.key());
1025            }
1026        }
1027
1028        let removed_count = stale_clients.len();
1029        for client_id in stale_clients {
1030            self.clients.remove(&client_id);
1031            info!("Removed stale client {}", client_id);
1032        }
1033
1034        removed_count
1035    }
1036
1037    /// Evict aggregate account usage and policy state for accounts with no
1038    /// live connection past the idle TTL.
1039    fn cleanup_account_state(&self) {
1040        let live_accounts: HashSet<String> = self
1041            .clients
1042            .iter()
1043            .filter_map(|entry| {
1044                entry
1045                    .value()
1046                    .auth_context
1047                    .as_ref()
1048                    .and_then(|ctx| ctx.account_key.clone())
1049            })
1050            .collect();
1051        self.account_usage.retain(|account, usage| {
1052            live_accounts.contains(account) || usage.idle_for() < ACCOUNT_USAGE_IDLE_TTL
1053        });
1054        self.account_policies
1055            .evict_idle(|account| live_accounts.contains(account));
1056    }
1057
1058    /// Start a background task that periodically cleans up stale clients.
1059    ///
1060    /// Returns the task so an owner that stops serving can abort it; callers
1061    /// that run for the life of the process may simply drop the handle.
1062    pub fn start_cleanup_task(&self) -> tokio::task::JoinHandle<()> {
1063        let client_manager = self.clone();
1064
1065        tokio::spawn(async move {
1066            let mut interval = tokio::time::interval(Duration::from_secs(30));
1067
1068            loop {
1069                interval.tick().await;
1070                let removed = client_manager.cleanup_stale_clients();
1071                if removed > 0 {
1072                    info!("Cleaned up {} stale clients", removed);
1073                }
1074                client_manager.cleanup_account_state();
1075            }
1076        })
1077    }
1078
1079    /// ENFORCEMENT HOOKS
1080    ///
1081    /// These methods provide hooks for enforcing limits based on auth context.
1082    /// They check limits before allowing operations and return errors if limits are exceeded.
1083    /// Check if a connection is allowed for the given auth context.
1084    ///
1085    /// Returns Ok(()) if the connection is allowed, or an error with a reason if not.
1086    #[allow(clippy::result_large_err)]
1087    pub async fn check_connection_allowed(
1088        &self,
1089        remote_addr: SocketAddr,
1090        auth_context: &Option<AuthContext>,
1091    ) -> Result<(), AuthDeny> {
1092        // Check rate limiter first if configured
1093        if let Some(ref rate_limiter) = self.rate_limiter {
1094            // Check handshake rate limit for IP
1095            match rate_limiter.check_handshake(remote_addr).await {
1096                RateLimitResult::Allowed { .. } => {}
1097                RateLimitResult::Denied { retry_after, limit } => {
1098                    return Err(AuthDeny::rate_limited(retry_after, "websocket handshakes")
1099                        .with_context(format!(
1100                            "handshake rate limit of {} per minute exceeded for {}",
1101                            limit, remote_addr
1102                        )));
1103                }
1104            }
1105
1106            if let Some(ref ctx) = auth_context {
1107                // Connection-attempt rate per resolved consumer. Legacy
1108                // tokens resolve to the subject with the configured window,
1109                // preserving old behavior exactly.
1110                match rate_limiter
1111                    .check_connection_for_consumer(
1112                        ctx.consumer_key(),
1113                        ctx.limits.max_connection_attempts_per_minute,
1114                    )
1115                    .await
1116                {
1117                    RateLimitResult::Allowed { .. } => {}
1118                    RateLimitResult::Denied { retry_after, limit } => {
1119                        return Err(AuthDeny::rate_limited(retry_after, "websocket connections")
1120                            .with_context(format!(
1121                                "connection rate limit for consumer {} of {} per minute exceeded",
1122                                ctx.consumer_key(),
1123                                limit
1124                            )));
1125                    }
1126                }
1127
1128                // Connection-attempt rate per resolved account. Legacy
1129                // tokens resolve to the metering key; anonymous v2 tokens
1130                // carry no account and skip the aggregate bucket.
1131                if ctx.is_legacy_policy() || ctx.account_key.is_some() {
1132                    match rate_limiter
1133                        .check_connection_for_account(
1134                            ctx.account_key(),
1135                            ctx.account_limits.max_connection_attempts_per_minute,
1136                        )
1137                        .await
1138                    {
1139                        RateLimitResult::Allowed { .. } => {}
1140                        RateLimitResult::Denied { retry_after, limit } => {
1141                            return Err(AuthDeny::rate_limited(
1142                                retry_after,
1143                                "metered websocket connections",
1144                            )
1145                            .with_context(format!(
1146                                "connection rate limit for account {} of {} per minute exceeded",
1147                                ctx.account_key(),
1148                                limit
1149                            )));
1150                        }
1151                    }
1152                }
1153            }
1154        }
1155
1156        // Check global per-IP connection limit
1157        if let Some(max_per_ip) = self.rate_limit_config.max_connections_per_ip {
1158            let current_ip_connections = self.count_connections_for_ip(&remote_addr);
1159            if current_ip_connections >= max_per_ip {
1160                return Err(AuthDeny::connection_limit_exceeded(
1161                    &format!("ip {}", remote_addr.ip()),
1162                    current_ip_connections,
1163                    max_per_ip,
1164                ));
1165            }
1166        }
1167
1168        if let Some(ctx) = auth_context {
1169            // Admit the token against previously observed account policy and
1170            // count legacy tokens so Plan 030 can end compatibility.
1171            if ctx.is_legacy_policy() {
1172                let legacy_policy_token = self.account_policies.record_legacy_token();
1173                debug!(legacy_policy_token, "legacy policy token admitted");
1174            } else if let (Some(account), Some(policy_version)) =
1175                (ctx.account_key.as_deref(), ctx.policy_version)
1176            {
1177                match self
1178                    .account_policies
1179                    .observe(account, policy_version, &ctx.account_limits)
1180                {
1181                    Ok(()) => {}
1182                    Err(AccountPolicyError::StaleVersion { presented, current }) => {
1183                        debug!(
1184                            account = %redact_identity(account),
1185                            presented,
1186                            current,
1187                            "stale policy version rejected"
1188                        );
1189                        return Err(AuthDeny::new(
1190                            crate::websocket::auth::AuthErrorCode::TokenExpired,
1191                            "session policy version is stale; refresh the session token",
1192                        ));
1193                    }
1194                    Err(AccountPolicyError::ConflictingLimits { version }) => {
1195                        warn!(
1196                            account = %redact_identity(account),
1197                            version,
1198                            "signed account limits conflict for one policy version"
1199                        );
1200                        return Err(AuthDeny::new(
1201                            crate::websocket::auth::AuthErrorCode::InternalError,
1202                            "signed account limits conflict with previously observed policy",
1203                        ));
1204                    }
1205                    Err(AccountPolicyError::CapacityExhausted) => {
1206                        warn!("account policy state at capacity; denying admission");
1207                        return Err(AuthDeny::new(
1208                            crate::websocket::auth::AuthErrorCode::InternalError,
1209                            "account policy state is at capacity; retry shortly",
1210                        ));
1211                    }
1212                }
1213            }
1214
1215            // Check max connections per resolved consumer (token limits,
1216            // fallback to default limits). Legacy tokens resolve to the
1217            // subject, preserving old behavior.
1218            let max_connections = ctx.limits.max_connections.or_else(|| {
1219                self.rate_limit_config
1220                    .default_limits
1221                    .as_ref()
1222                    .and_then(|l| l.max_connections)
1223            });
1224            if let Some(max_connections) = max_connections {
1225                let current_connections = self.count_connections_for_consumer(ctx.consumer_key());
1226                if current_connections >= max_connections as usize {
1227                    return Err(AuthDeny::connection_limit_exceeded(
1228                        &format!("consumer {}", ctx.consumer_key()),
1229                        current_connections,
1230                        max_connections as usize,
1231                    ));
1232                }
1233            }
1234
1235            // Check aggregate concurrent connections per account from the
1236            // signed account limits.
1237            if !ctx.is_legacy_policy() && ctx.account_key.is_some() {
1238                if let Some(max_account_connections) = ctx.account_limits.max_connections {
1239                    let current_account_connections =
1240                        self.count_connections_for_account(ctx.account_key());
1241                    if current_account_connections >= max_account_connections as usize {
1242                        return Err(AuthDeny::connection_limit_exceeded(
1243                            &format!("account {}", ctx.account_key()),
1244                            current_account_connections,
1245                            max_account_connections as usize,
1246                        ));
1247                    }
1248                }
1249            }
1250
1251            // Check global max connections per metering key
1252            if let Some(max_per_metering_key) =
1253                self.rate_limit_config.max_connections_per_metering_key
1254            {
1255                let current_metering_connections =
1256                    self.count_connections_for_metering_key(&ctx.metering_key);
1257                if current_metering_connections >= max_per_metering_key {
1258                    return Err(AuthDeny::connection_limit_exceeded(
1259                        &format!("metering key {}", ctx.metering_key),
1260                        current_metering_connections,
1261                        max_per_metering_key,
1262                    ));
1263                }
1264            }
1265
1266            // Check global max connections per origin
1267            if let Some(max_per_origin) = self.rate_limit_config.max_connections_per_origin {
1268                if let Some(ref origin) = ctx.origin {
1269                    let current_origin_connections = self.count_connections_for_origin(origin);
1270                    if current_origin_connections >= max_per_origin {
1271                        return Err(AuthDeny::connection_limit_exceeded(
1272                            &format!("origin {}", origin),
1273                            current_origin_connections,
1274                            max_per_origin,
1275                        ));
1276                    }
1277                }
1278            }
1279        }
1280        Ok(())
1281    }
1282
1283    /// Count connections from a specific IP address
1284    fn count_connections_for_ip(&self, remote_addr: &SocketAddr) -> usize {
1285        let ip = remote_addr.ip();
1286        self.clients
1287            .iter()
1288            .filter(|entry| entry.value().remote_addr.ip() == ip)
1289            .count()
1290    }
1291
1292    /// Count connections for a resolved consumer identity
1293    fn count_connections_for_consumer(&self, consumer: &str) -> usize {
1294        self.clients
1295            .iter()
1296            .filter(|entry| {
1297                entry
1298                    .value()
1299                    .auth_context
1300                    .as_ref()
1301                    .map(|ctx| ctx.consumer_key() == consumer)
1302                    .unwrap_or(false)
1303            })
1304            .count()
1305    }
1306
1307    /// Count connections for a resolved account identity
1308    fn count_connections_for_account(&self, account: &str) -> usize {
1309        self.clients
1310            .iter()
1311            .filter(|entry| {
1312                entry
1313                    .value()
1314                    .auth_context
1315                    .as_ref()
1316                    .map(|ctx| ctx.account_key() == account)
1317                    .unwrap_or(false)
1318            })
1319            .count()
1320    }
1321
1322    /// Count connections for a specific metering key
1323    fn count_connections_for_metering_key(&self, metering_key: &str) -> usize {
1324        self.clients
1325            .iter()
1326            .filter(|entry| {
1327                entry
1328                    .value()
1329                    .auth_context
1330                    .as_ref()
1331                    .map(|ctx| ctx.metering_key == metering_key)
1332                    .unwrap_or(false)
1333            })
1334            .count()
1335    }
1336
1337    /// Count connections for a specific origin
1338    fn count_connections_for_origin(&self, origin: &str) -> usize {
1339        self.clients
1340            .iter()
1341            .filter(|entry| {
1342                entry
1343                    .value()
1344                    .auth_context
1345                    .as_ref()
1346                    .and_then(|ctx| ctx.origin.as_ref())
1347                    .map(|o| o == origin)
1348                    .unwrap_or(false)
1349            })
1350            .count()
1351    }
1352
1353    /// Check if a subscription is allowed for the given client.
1354    ///
1355    /// Returns Ok(()) if the subscription is allowed, or an error with a reason if not.
1356    #[allow(clippy::result_large_err)]
1357    pub async fn check_subscription_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
1358        let context = {
1359            let Some(client) = self.clients.get(&client_id) else {
1360                return Ok(());
1361            };
1362            let current_subs = client.subscription_count().await;
1363
1364            // Check max subscriptions per connection (use token limits, fallback to default limits)
1365            if let Some(ref ctx) = client.auth_context {
1366                let max_subs = ctx.limits.max_subscriptions.or_else(|| {
1367                    self.rate_limit_config
1368                        .default_limits
1369                        .as_ref()
1370                        .and_then(|l| l.max_subscriptions)
1371                });
1372                if let Some(max_subs) = max_subs {
1373                    if current_subs >= max_subs as usize {
1374                        return Err(AuthDeny::new(
1375                            crate::websocket::auth::AuthErrorCode::SubscriptionLimitExceeded,
1376                            format!(
1377                                "Subscription limit exceeded: {} of {} subscriptions for client {}",
1378                                current_subs, max_subs, client_id
1379                            ),
1380                        )
1381                        .with_suggested_action(
1382                            "Unsubscribe from an existing view before creating another subscription",
1383                        ));
1384                    }
1385                }
1386            }
1387            client.auth_context.clone()
1388        };
1389
1390        // Signed subscription-create rates, enforced only for v2 tokens that
1391        // carry the corresponding optional limit fields.
1392        if let (Some(rate_limiter), Some(ctx)) = (self.rate_limiter.as_ref(), context.as_ref()) {
1393            if !ctx.is_legacy_policy() {
1394                if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1395                    .check_subscription_create_for_consumer(
1396                        ctx.consumer_key(),
1397                        ctx.limits.max_subscription_creates_per_minute,
1398                    )
1399                    .await
1400                {
1401                    return Err(AuthDeny::rate_limited(retry_after, "subscription creates")
1402                        .with_context(format!(
1403                        "subscription-create rate limit for consumer {} of {} per minute exceeded",
1404                        ctx.consumer_key(),
1405                        limit
1406                    )));
1407                }
1408
1409                if ctx.account_key.is_some() {
1410                    if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1411                        .check_subscription_create_for_account(
1412                            ctx.account_key(),
1413                            ctx.account_limits.max_subscription_creates_per_minute,
1414                        )
1415                        .await
1416                    {
1417                        return Err(AuthDeny::rate_limited(
1418                            retry_after,
1419                            "account subscription creates",
1420                        )
1421                        .with_context(format!(
1422                            "subscription-create rate limit for account {} of {} per minute exceeded",
1423                            ctx.account_key(),
1424                            limit
1425                        )));
1426                    }
1427                }
1428            }
1429        }
1430        Ok(())
1431    }
1432
1433    /// Get metering key for a client
1434    pub fn get_metering_key(&self, client_id: Uuid) -> Option<String> {
1435        self.clients.get(&client_id).and_then(|client| {
1436            client
1437                .auth_context
1438                .as_ref()
1439                .map(|ctx| ctx.metering_key.clone())
1440        })
1441    }
1442
1443    /// Get auth context for a client.
1444    pub fn get_auth_context(&self, client_id: Uuid) -> Option<AuthContext> {
1445        self.clients
1446            .get(&client_id)
1447            .and_then(|client| client.auth_context.clone())
1448    }
1449
1450    /// Check if a snapshot request is allowed (based on max_snapshot_rows limit)
1451    ///
1452    /// Uses token limits if available, falls back to default limits from RateLimitConfig.
1453    #[allow(clippy::result_large_err)]
1454    pub fn check_snapshot_allowed(
1455        &self,
1456        client_id: Uuid,
1457        requested_rows: u32,
1458    ) -> Result<(), AuthDeny> {
1459        if let Some(client) = self.clients.get(&client_id) {
1460            if let Some(ref ctx) = client.auth_context {
1461                let max_rows = ctx.limits.max_snapshot_rows.or_else(|| {
1462                    self.rate_limit_config
1463                        .default_limits
1464                        .as_ref()
1465                        .and_then(|l| l.max_snapshot_rows)
1466                });
1467                if let Some(max_rows) = max_rows {
1468                    if requested_rows > max_rows {
1469                        return Err(AuthDeny::new(
1470                            crate::websocket::auth::AuthErrorCode::SnapshotLimitExceeded,
1471                            format!(
1472                                "Snapshot limit exceeded: requested {} rows, max allowed is {} for client {}",
1473                                requested_rows, max_rows, client_id
1474                            ),
1475                        )
1476                        .with_suggested_action(
1477                            "Request fewer rows or lower the snapshotLimit on the subscription",
1478                        ));
1479                    }
1480                }
1481            }
1482        }
1483        Ok(())
1484    }
1485}
1486
1487impl Default for ClientManager {
1488    fn default() -> Self {
1489        Self::new()
1490    }
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495    use super::*;
1496    use crate::websocket::auth::AuthContext;
1497    use arete_auth::{KeyClass, Limits};
1498    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
1499
1500    fn create_test_auth_context(subject: &str, limits: Limits) -> AuthContext {
1501        AuthContext {
1502            subject: subject.to_string(),
1503            issuer: "test-issuer".to_string(),
1504            audience: "test-audience".to_string(),
1505            key_class: KeyClass::Publishable,
1506            metering_key: format!("meter-{}", subject),
1507            deployment_id: None,
1508            target_kind: None,
1509            target_id: None,
1510            program_id: None,
1511            program_release_hash: None,
1512            expires_at: u64::MAX,
1513            scope: "read".to_string(),
1514            limits,
1515            plan: None,
1516            origin: None,
1517            client_ip: None,
1518            jti: uuid::Uuid::new_v4().to_string(),
1519            actor_key: None,
1520            account_key: None,
1521            consumer_key: None,
1522            policy_version: None,
1523            account_limits: Limits::default(),
1524        }
1525    }
1526
1527    fn create_v2_auth_context(
1528        consumer: &str,
1529        account: &str,
1530        policy_version: u32,
1531        limits: Limits,
1532        account_limits: Limits,
1533    ) -> AuthContext {
1534        AuthContext {
1535            subject: "user:1".to_string(),
1536            issuer: "test-issuer".to_string(),
1537            audience: "test-audience".to_string(),
1538            key_class: KeyClass::Publishable,
1539            metering_key: account.to_string(),
1540            deployment_id: None,
1541            target_kind: None,
1542            target_id: None,
1543            program_id: None,
1544            program_release_hash: None,
1545            expires_at: u64::MAX,
1546            scope: "read".to_string(),
1547            limits,
1548            plan: Some("pro".to_string()),
1549            origin: None,
1550            client_ip: None,
1551            jti: uuid::Uuid::new_v4().to_string(),
1552            actor_key: Some("user:1".to_string()),
1553            account_key: Some(account.to_string()),
1554            consumer_key: Some(consumer.to_string()),
1555            policy_version: Some(policy_version),
1556            account_limits,
1557        }
1558    }
1559
1560    fn insert_client(manager: &ClientManager, context: AuthContext) -> Uuid {
1561        let (sender, receiver) = mpsc::channel(8);
1562        // Keep the receiver alive so the sender stays open.
1563        std::mem::forget(receiver);
1564        let client_id = Uuid::new_v4();
1565        manager.clients.insert(
1566            client_id,
1567            ClientInfo::new(
1568                client_id,
1569                sender,
1570                Some(context),
1571                create_test_socket_addr("127.0.0.1"),
1572            ),
1573        );
1574        client_id
1575    }
1576
1577    fn create_test_socket_addr(ip: &str) -> SocketAddr {
1578        SocketAddr::new(
1579            ip.parse::<IpAddr>()
1580                .unwrap_or(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1581            12345,
1582        )
1583    }
1584
1585    #[test]
1586    fn test_egress_tracker_basic() {
1587        let mut tracker = EgressTracker::new();
1588
1589        // Should allow bytes within limit
1590        assert!(tracker.record_bytes(500, 1000));
1591        assert_eq!(tracker.current_usage(), 500);
1592
1593        // Should allow more bytes within limit
1594        assert!(tracker.record_bytes(400, 1000));
1595        assert_eq!(tracker.current_usage(), 900);
1596
1597        // Should reject bytes over limit
1598        assert!(!tracker.record_bytes(200, 1000));
1599        assert_eq!(tracker.current_usage(), 900); // Usage shouldn't increase
1600    }
1601
1602    #[test]
1603    fn test_egress_tracker_window_reset() {
1604        let mut tracker = EgressTracker::new();
1605
1606        // Use up the limit
1607        assert!(tracker.record_bytes(100, 100));
1608        assert!(!tracker.record_bytes(1, 100));
1609
1610        // Reset the window
1611        tracker.bytes_this_minute = 0;
1612        tracker.window_start = SystemTime::now() - Duration::from_secs(61);
1613
1614        // Should allow after window reset
1615        assert!(tracker.record_bytes(50, 100));
1616    }
1617
1618    #[test]
1619    fn test_message_rate_tracker_basic() {
1620        let mut tracker = MessageRateTracker::new();
1621
1622        assert!(tracker.record_message(2));
1623        assert_eq!(tracker.current_usage(), 1);
1624
1625        assert!(tracker.record_message(2));
1626        assert_eq!(tracker.current_usage(), 2);
1627
1628        assert!(!tracker.record_message(2));
1629        assert_eq!(tracker.current_usage(), 2);
1630    }
1631
1632    #[tokio::test]
1633    async fn test_client_inbound_message_limit() {
1634        let (tx, _rx) = mpsc::channel(1);
1635        let client = ClientInfo::new(
1636            Uuid::new_v4(),
1637            tx,
1638            Some(create_test_auth_context(
1639                "user-1",
1640                Limits {
1641                    max_messages_per_minute: Some(2),
1642                    ..Default::default()
1643                },
1644            )),
1645            create_test_socket_addr("127.0.0.1"),
1646        );
1647
1648        assert_eq!(client.record_inbound_message(), Some(1));
1649        assert_eq!(client.record_inbound_message(), Some(2));
1650        assert_eq!(client.record_inbound_message(), None);
1651    }
1652
1653    #[tokio::test]
1654    async fn duplicate_subscription_id_is_rejected_without_replacement() {
1655        let (tx, _rx) = mpsc::channel(1);
1656        let client = ClientInfo::new(
1657            Uuid::new_v4(),
1658            tx,
1659            None,
1660            create_test_socket_addr("127.0.0.1"),
1661        );
1662        let first = CancellationToken::new();
1663        let duplicate = CancellationToken::new();
1664
1665        assert!(
1666            client
1667                .add_subscription("opaque-id".to_string(), first.clone())
1668                .await
1669        );
1670        assert!(
1671            !client
1672                .add_subscription("opaque-id".to_string(), duplicate.clone())
1673                .await
1674        );
1675        assert!(!first.is_cancelled());
1676        assert!(!duplicate.is_cancelled());
1677
1678        assert!(client.remove_subscription("opaque-id").await);
1679        assert!(first.is_cancelled());
1680        assert!(!duplicate.is_cancelled());
1681    }
1682
1683    #[tokio::test]
1684    async fn test_no_limits() {
1685        let manager = ClientManager::new();
1686        let addr = create_test_socket_addr("127.0.0.1");
1687
1688        // No auth context - should succeed
1689        assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1690
1691        // Auth context with no limits - should succeed
1692        let auth_context = create_test_auth_context("test", Limits::default());
1693        assert!(manager
1694            .check_connection_allowed(addr, &Some(auth_context))
1695            .await
1696            .is_ok());
1697    }
1698
1699    #[tokio::test]
1700    async fn test_per_subject_connection_limit() {
1701        let manager = ClientManager::new();
1702
1703        let limits = Limits {
1704            max_connections: Some(2),
1705            ..Default::default()
1706        };
1707
1708        let auth_context = create_test_auth_context("user-1", limits);
1709        let addr = create_test_socket_addr("127.0.0.1");
1710
1711        // First connection should succeed (no clients yet)
1712        assert!(manager
1713            .check_connection_allowed(addr, &Some(auth_context.clone()))
1714            .await
1715            .is_ok());
1716    }
1717
1718    #[tokio::test]
1719    async fn test_per_ip_connection_limit() {
1720        let manager = ClientManager::new().with_max_connections_per_ip(2);
1721        let addr = create_test_socket_addr("192.168.1.1");
1722
1723        // Should succeed when no connections from that IP
1724        assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1725    }
1726
1727    // Tests for RateLimitConfig
1728    #[test]
1729    fn rate_limit_config_default() {
1730        let config = RateLimitConfig::default();
1731        assert!(config.max_connections_per_ip.is_none());
1732        assert_eq!(config.client_timeout, Duration::from_secs(300));
1733        assert_eq!(config.message_queue_size, 512);
1734        assert!(config.max_reconnect_attempts.is_none());
1735        assert_eq!(config.message_rate_window, Duration::from_secs(60));
1736        assert_eq!(config.egress_rate_window, Duration::from_secs(60));
1737    }
1738
1739    #[test]
1740    fn rate_limit_config_builder_methods() {
1741        let config = RateLimitConfig::default()
1742            .with_max_connections_per_ip(10)
1743            .with_timeout(Duration::from_secs(600))
1744            .with_message_queue_size(1024)
1745            .with_rate_limit_window(Duration::from_secs(120));
1746
1747        assert_eq!(config.max_connections_per_ip, Some(10));
1748        assert_eq!(config.client_timeout, Duration::from_secs(600));
1749        assert_eq!(config.message_queue_size, 1024);
1750        assert_eq!(config.message_rate_window, Duration::from_secs(120));
1751        assert_eq!(config.egress_rate_window, Duration::from_secs(120));
1752    }
1753
1754    #[tokio::test]
1755    async fn client_manager_with_config() {
1756        let config = RateLimitConfig::default()
1757            .with_max_connections_per_ip(5)
1758            .with_timeout(Duration::from_secs(120))
1759            .with_message_queue_size(256);
1760
1761        let manager = ClientManager::with_config(config);
1762        let addr = create_test_socket_addr("10.0.0.1");
1763
1764        // Check that the configuration was applied
1765        assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(5));
1766        assert_eq!(
1767            manager.rate_limit_config().client_timeout,
1768            Duration::from_secs(120)
1769        );
1770        assert_eq!(manager.rate_limit_config().message_queue_size, 256);
1771
1772        // Should allow when under limit
1773        assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1774    }
1775
1776    #[tokio::test]
1777    async fn client_manager_builder_pattern() {
1778        let manager = ClientManager::new()
1779            .with_max_connections_per_ip(10)
1780            .with_timeout(Duration::from_secs(180))
1781            .with_message_queue_size(1024)
1782            .with_rate_limit_window(Duration::from_secs(90));
1783
1784        assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(10));
1785        assert_eq!(
1786            manager.rate_limit_config().client_timeout,
1787            Duration::from_secs(180)
1788        );
1789        assert_eq!(manager.rate_limit_config().message_queue_size, 1024);
1790        assert_eq!(
1791            manager.rate_limit_config().message_rate_window,
1792            Duration::from_secs(90)
1793        );
1794    }
1795
1796    // Integration test: Connection limits are enforced
1797    #[tokio::test]
1798    async fn connection_limit_enforcement_with_actual_clients() {
1799        let manager = ClientManager::new().with_max_connections_per_ip(2);
1800        let addr1 = create_test_socket_addr("192.168.1.1");
1801        let addr2 = create_test_socket_addr("192.168.1.2");
1802
1803        // First connection from IP1 should succeed
1804        let auth1 = create_test_auth_context("user-1", Limits::default());
1805        assert!(manager
1806            .check_connection_allowed(addr1, &Some(auth1.clone()))
1807            .await
1808            .is_ok());
1809
1810        // Simulate adding a client (we can't easily do this without a real WebSocket,
1811        // but we can verify the check logic works)
1812
1813        // Same IP, different auth context - should still count toward IP limit
1814        let auth2 = create_test_auth_context("user-2", Limits::default());
1815        assert!(manager
1816            .check_connection_allowed(addr1, &Some(auth2.clone()))
1817            .await
1818            .is_ok());
1819
1820        // Different IP - should succeed regardless
1821        let auth3 = create_test_auth_context("user-3", Limits::default());
1822        assert!(manager
1823            .check_connection_allowed(addr2, &Some(auth3.clone()))
1824            .await
1825            .is_ok());
1826    }
1827
1828    // Test subscription limit enforcement
1829    #[tokio::test]
1830    async fn subscription_limit_enforcement() {
1831        let manager = ClientManager::new();
1832        let addr = create_test_socket_addr("127.0.0.1");
1833
1834        // Create auth context with subscription limit
1835        let auth = create_test_auth_context(
1836            "user-1",
1837            Limits {
1838                max_subscriptions: Some(2),
1839                ..Default::default()
1840            },
1841        );
1842
1843        // Check should pass initially
1844        assert!(manager
1845            .check_connection_allowed(addr, &Some(auth.clone()))
1846            .await
1847            .is_ok());
1848
1849        // Note: We can't easily test the full subscription flow without a real connection,
1850        // but we verify the limit configuration is properly stored
1851        assert_eq!(auth.limits.max_subscriptions, Some(2));
1852    }
1853
1854    // Test snapshot limit enforcement
1855    #[tokio::test]
1856    async fn snapshot_limit_enforcement() {
1857        let manager = ClientManager::new();
1858        let addr = create_test_socket_addr("127.0.0.1");
1859
1860        let auth = create_test_auth_context(
1861            "user-1",
1862            Limits {
1863                max_snapshot_rows: Some(1000),
1864                ..Default::default()
1865            },
1866        );
1867
1868        assert!(manager
1869            .check_connection_allowed(addr, &Some(auth.clone()))
1870            .await
1871            .is_ok());
1872
1873        // Note: Actual snapshot limit checking happens in check_snapshot_allowed
1874        // which requires a connected client
1875    }
1876
1877    #[tokio::test]
1878    async fn two_consumers_share_the_account_connection_cap() {
1879        let manager = ClientManager::new();
1880        let account_limits = Limits {
1881            max_connections: Some(1),
1882            ..Limits::default()
1883        };
1884        let consumer_a = create_v2_auth_context(
1885            "consumer:a",
1886            "account:42",
1887            1,
1888            Limits::default(),
1889            account_limits.clone(),
1890        );
1891        insert_client(&manager, consumer_a);
1892
1893        // A different consumer on the same account is blocked by the
1894        // aggregate cap.
1895        let consumer_b = create_v2_auth_context(
1896            "consumer:b",
1897            "account:42",
1898            1,
1899            Limits::default(),
1900            account_limits.clone(),
1901        );
1902        let addr = create_test_socket_addr("127.0.0.1");
1903        assert!(manager
1904            .check_connection_allowed(addr, &Some(consumer_b))
1905            .await
1906            .is_err());
1907
1908        // A consumer on another account is unaffected.
1909        let other_account = create_v2_auth_context(
1910            "consumer:c",
1911            "account:43",
1912            1,
1913            Limits::default(),
1914            account_limits,
1915        );
1916        assert!(manager
1917            .check_connection_allowed(addr, &Some(other_account))
1918            .await
1919            .is_ok());
1920    }
1921
1922    #[tokio::test]
1923    async fn one_consumer_is_limited_independently_of_its_account() {
1924        let manager = ClientManager::new();
1925        let limits = Limits {
1926            max_connections: Some(1),
1927            ..Limits::default()
1928        };
1929        let account_limits = Limits {
1930            max_connections: Some(10),
1931            ..Limits::default()
1932        };
1933        let consumer_a = create_v2_auth_context(
1934            "consumer:a",
1935            "account:42",
1936            1,
1937            limits.clone(),
1938            account_limits.clone(),
1939        );
1940        insert_client(&manager, consumer_a.clone());
1941
1942        let addr = create_test_socket_addr("127.0.0.1");
1943        // The same consumer hits its own cap.
1944        assert!(manager
1945            .check_connection_allowed(addr, &Some(consumer_a))
1946            .await
1947            .is_err());
1948
1949        // A sibling consumer under the same account is still admitted.
1950        let consumer_b =
1951            create_v2_auth_context("consumer:b", "account:42", 1, limits, account_limits);
1952        assert!(manager
1953            .check_connection_allowed(addr, &Some(consumer_b))
1954            .await
1955            .is_ok());
1956    }
1957
1958    #[tokio::test]
1959    async fn policy_version_upgrade_stale_and_conflict_rules_apply() {
1960        let manager = ClientManager::new();
1961        let addr = create_test_socket_addr("127.0.0.1");
1962        let limits_v1 = Limits {
1963            max_connections: Some(5),
1964            ..Limits::default()
1965        };
1966        let limits_v2 = Limits {
1967            max_connections: Some(2),
1968            ..Limits::default()
1969        };
1970
1971        // Version 1 admits and creates state.
1972        let v1 = create_v2_auth_context(
1973            "consumer:a",
1974            "account:42",
1975            1,
1976            Limits::default(),
1977            limits_v1.clone(),
1978        );
1979        assert!(manager
1980            .check_connection_allowed(addr, &Some(v1.clone()))
1981            .await
1982            .is_ok());
1983
1984        // Version 2 with new limits replaces the policy.
1985        let v2 = create_v2_auth_context(
1986            "consumer:a",
1987            "account:42",
1988            2,
1989            Limits::default(),
1990            limits_v2.clone(),
1991        );
1992        assert!(manager
1993            .check_connection_allowed(addr, &Some(v2))
1994            .await
1995            .is_ok());
1996
1997        // A stale version-1 token is rejected once version 2 was observed.
1998        let stale = manager.check_connection_allowed(addr, &Some(v1)).await;
1999        assert!(stale.is_err());
2000
2001        // Version 2 with different limits is a signing/config fault.
2002        let conflict =
2003            create_v2_auth_context("consumer:a", "account:42", 2, Limits::default(), limits_v1);
2004        assert!(manager
2005            .check_connection_allowed(addr, &Some(conflict))
2006            .await
2007            .is_err());
2008    }
2009
2010    #[tokio::test]
2011    async fn legacy_tokens_keep_old_behavior_and_are_counted() {
2012        let manager = ClientManager::new();
2013        let addr = create_test_socket_addr("127.0.0.1");
2014        let legacy = create_test_auth_context(
2015            "user-1",
2016            Limits {
2017                max_connections: Some(2),
2018                ..Limits::default()
2019            },
2020        );
2021        assert!(legacy.is_legacy_policy());
2022
2023        assert!(manager
2024            .check_connection_allowed(addr, &Some(legacy.clone()))
2025            .await
2026            .is_ok());
2027        assert_eq!(manager.account_policies().legacy_token_count(), 1);
2028        // Legacy tokens create no account policy state.
2029        assert_eq!(manager.account_policies().tracked_accounts(), 0);
2030
2031        // Legacy connection counting still keys off the subject.
2032        insert_client(&manager, legacy.clone());
2033        insert_client(&manager, legacy.clone());
2034        assert!(manager
2035            .check_connection_allowed(addr, &Some(legacy))
2036            .await
2037            .is_err());
2038    }
2039
2040    #[tokio::test]
2041    async fn account_message_budget_is_shared_across_consumers() {
2042        let manager = ClientManager::new();
2043        let account_limits = Limits {
2044            max_messages_per_minute: Some(2),
2045            ..Limits::default()
2046        };
2047        let client_a = insert_client(
2048            &manager,
2049            create_v2_auth_context(
2050                "consumer:a",
2051                "account:42",
2052                1,
2053                Limits::default(),
2054                account_limits.clone(),
2055            ),
2056        );
2057        let client_b = insert_client(
2058            &manager,
2059            create_v2_auth_context(
2060                "consumer:b",
2061                "account:42",
2062                1,
2063                Limits::default(),
2064                account_limits,
2065            ),
2066        );
2067
2068        assert!(manager.check_inbound_message_allowed(client_a).is_ok());
2069        assert!(manager.check_inbound_message_allowed(client_b).is_ok());
2070        // The third message anywhere on the account is rejected.
2071        assert!(manager.check_inbound_message_allowed(client_a).is_err());
2072    }
2073
2074    #[tokio::test]
2075    async fn account_state_evicts_when_idle_and_unreferenced() {
2076        let manager = ClientManager::new();
2077        let addr = create_test_socket_addr("127.0.0.1");
2078        let context = create_v2_auth_context(
2079            "consumer:a",
2080            "account:42",
2081            1,
2082            Limits::default(),
2083            Limits::default(),
2084        );
2085        assert!(manager
2086            .check_connection_allowed(addr, &Some(context))
2087            .await
2088            .is_ok());
2089        assert_eq!(manager.account_policies().tracked_accounts(), 1);
2090
2091        // No live connection references the account; an idle sweep with a
2092        // zero TTL registry drops it.
2093        manager.account_policies().evict_idle(|_| false);
2094        assert_eq!(
2095            manager.account_policies().tracked_accounts(),
2096            1,
2097            "TTL keeps fresh entries"
2098        );
2099        manager.cleanup_account_state();
2100        assert_eq!(
2101            manager.account_policies().tracked_accounts(),
2102            1,
2103            "fresh entries survive sweep"
2104        );
2105    }
2106
2107    #[test]
2108    fn expired_token_is_removed_without_deadlocking() {
2109        let manager = ClientManager::new();
2110        let mut context = create_test_auth_context("user-1", Limits::default());
2111        context.expires_at = 1;
2112        let client_id = insert_client(&manager, context);
2113
2114        // Send from another thread: a deadlock blocks that thread for good,
2115        // so the test fails on the timeout instead of hanging.
2116        let (done_tx, done_rx) = std::sync::mpsc::channel();
2117        let sender = manager.clone();
2118        std::thread::spawn(move || {
2119            let result = sender.send_to_client(client_id, Arc::new(Bytes::from_static(b"update")));
2120            let _ = done_tx.send(result);
2121        });
2122
2123        let result = done_rx
2124            .recv_timeout(Duration::from_secs(5))
2125            .expect("send_to_client deadlocked on an expired token");
2126        assert_eq!(result, Err(SendError::ClientDisconnected));
2127        assert!(!manager.has_client(client_id));
2128    }
2129
2130    #[tokio::test]
2131    async fn an_expired_client_keeps_its_close_reason_when_its_queue_is_full() {
2132        let manager = ClientManager::new();
2133        let (sender, mut queue) = mpsc::channel(1);
2134        sender
2135            .try_send(Message::Text("backlog".into()))
2136            .expect("room for one message");
2137        let client_id = Uuid::new_v4();
2138        let mut context = create_test_auth_context("user-1", Limits::default());
2139        context.expires_at = 1;
2140        let client = ClientInfo::new(
2141            client_id,
2142            sender,
2143            Some(context),
2144            create_test_socket_addr("127.0.0.1"),
2145        );
2146        let close_frame = client.close_frame.clone();
2147        manager.clients.insert(client_id, client);
2148
2149        assert!(manager.check_and_remove_expired(client_id));
2150
2151        let frame = close_frame
2152            .get()
2153            .expect("the close reason is recorded even though the queue is full");
2154        assert_eq!(frame.code, CloseCode::Policy);
2155        assert_eq!(
2156            frame.reason.as_str(),
2157            "token-expired: Authentication token expired"
2158        );
2159        // The backlog is still delivered first, and removing the client
2160        // dropped its sender, so the queue then ends and the sender task
2161        // closes the socket with the recorded reason.
2162        assert!(matches!(queue.recv().await, Some(Message::Text(_))));
2163        assert!(queue.recv().await.is_none());
2164    }
2165
2166    #[test]
2167    fn unexpired_token_is_kept() {
2168        let manager = ClientManager::new();
2169        let client_id = insert_client(
2170            &manager,
2171            create_test_auth_context("user-1", Limits::default()),
2172        );
2173
2174        assert!(!manager.check_and_remove_expired(client_id));
2175        assert!(manager.has_client(client_id));
2176    }
2177
2178    // Test WebSocketRateLimiter integration
2179    #[tokio::test]
2180    async fn test_rate_limiter_integration() {
2181        use crate::websocket::rate_limiter::{RateLimiterConfig, WebSocketRateLimiter};
2182
2183        let rate_limiter = Arc::new(WebSocketRateLimiter::new(RateLimiterConfig::default()));
2184        let manager = ClientManager::new().with_rate_limiter(rate_limiter);
2185        let addr = create_test_socket_addr("127.0.0.1");
2186
2187        // Should allow connections when rate limiter is configured
2188        let auth = create_test_auth_context("user-1", Limits::default());
2189        assert!(manager
2190            .check_connection_allowed(addr, &Some(auth))
2191            .await
2192            .is_ok());
2193    }
2194}