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