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    ///
1028    /// Returns the task so an owner that stops serving can abort it; callers
1029    /// that run for the life of the process may simply drop the handle.
1030    pub fn start_cleanup_task(&self) -> tokio::task::JoinHandle<()> {
1031        let client_manager = self.clone();
1032
1033        tokio::spawn(async move {
1034            let mut interval = tokio::time::interval(Duration::from_secs(30));
1035
1036            loop {
1037                interval.tick().await;
1038                let removed = client_manager.cleanup_stale_clients();
1039                if removed > 0 {
1040                    info!("Cleaned up {} stale clients", removed);
1041                }
1042                client_manager.cleanup_account_state();
1043            }
1044        })
1045    }
1046
1047    /// ENFORCEMENT HOOKS
1048    ///
1049    /// These methods provide hooks for enforcing limits based on auth context.
1050    /// They check limits before allowing operations and return errors if limits are exceeded.
1051    /// Check if a connection is allowed for the given auth context.
1052    ///
1053    /// Returns Ok(()) if the connection is allowed, or an error with a reason if not.
1054    #[allow(clippy::result_large_err)]
1055    pub async fn check_connection_allowed(
1056        &self,
1057        remote_addr: SocketAddr,
1058        auth_context: &Option<AuthContext>,
1059    ) -> Result<(), AuthDeny> {
1060        // Check rate limiter first if configured
1061        if let Some(ref rate_limiter) = self.rate_limiter {
1062            // Check handshake rate limit for IP
1063            match rate_limiter.check_handshake(remote_addr).await {
1064                RateLimitResult::Allowed { .. } => {}
1065                RateLimitResult::Denied { retry_after, limit } => {
1066                    return Err(AuthDeny::rate_limited(retry_after, "websocket handshakes")
1067                        .with_context(format!(
1068                            "handshake rate limit of {} per minute exceeded for {}",
1069                            limit, remote_addr
1070                        )));
1071                }
1072            }
1073
1074            if let Some(ref ctx) = auth_context {
1075                // Connection-attempt rate per resolved consumer. Legacy
1076                // tokens resolve to the subject with the configured window,
1077                // preserving old behavior exactly.
1078                match rate_limiter
1079                    .check_connection_for_consumer(
1080                        ctx.consumer_key(),
1081                        ctx.limits.max_connection_attempts_per_minute,
1082                    )
1083                    .await
1084                {
1085                    RateLimitResult::Allowed { .. } => {}
1086                    RateLimitResult::Denied { retry_after, limit } => {
1087                        return Err(AuthDeny::rate_limited(retry_after, "websocket connections")
1088                            .with_context(format!(
1089                                "connection rate limit for consumer {} of {} per minute exceeded",
1090                                ctx.consumer_key(),
1091                                limit
1092                            )));
1093                    }
1094                }
1095
1096                // Connection-attempt rate per resolved account. Legacy
1097                // tokens resolve to the metering key; anonymous v2 tokens
1098                // carry no account and skip the aggregate bucket.
1099                if ctx.is_legacy_policy() || ctx.account_key.is_some() {
1100                    match rate_limiter
1101                        .check_connection_for_account(
1102                            ctx.account_key(),
1103                            ctx.account_limits.max_connection_attempts_per_minute,
1104                        )
1105                        .await
1106                    {
1107                        RateLimitResult::Allowed { .. } => {}
1108                        RateLimitResult::Denied { retry_after, limit } => {
1109                            return Err(AuthDeny::rate_limited(
1110                                retry_after,
1111                                "metered websocket connections",
1112                            )
1113                            .with_context(format!(
1114                                "connection rate limit for account {} of {} per minute exceeded",
1115                                ctx.account_key(),
1116                                limit
1117                            )));
1118                        }
1119                    }
1120                }
1121            }
1122        }
1123
1124        // Check global per-IP connection limit
1125        if let Some(max_per_ip) = self.rate_limit_config.max_connections_per_ip {
1126            let current_ip_connections = self.count_connections_for_ip(&remote_addr);
1127            if current_ip_connections >= max_per_ip {
1128                return Err(AuthDeny::connection_limit_exceeded(
1129                    &format!("ip {}", remote_addr.ip()),
1130                    current_ip_connections,
1131                    max_per_ip,
1132                ));
1133            }
1134        }
1135
1136        if let Some(ctx) = auth_context {
1137            // Admit the token against previously observed account policy and
1138            // count legacy tokens so Plan 030 can end compatibility.
1139            if ctx.is_legacy_policy() {
1140                let legacy_policy_token = self.account_policies.record_legacy_token();
1141                debug!(legacy_policy_token, "legacy policy token admitted");
1142            } else if let (Some(account), Some(policy_version)) =
1143                (ctx.account_key.as_deref(), ctx.policy_version)
1144            {
1145                match self
1146                    .account_policies
1147                    .observe(account, policy_version, &ctx.account_limits)
1148                {
1149                    Ok(()) => {}
1150                    Err(AccountPolicyError::StaleVersion { presented, current }) => {
1151                        debug!(
1152                            account = %redact_identity(account),
1153                            presented,
1154                            current,
1155                            "stale policy version rejected"
1156                        );
1157                        return Err(AuthDeny::new(
1158                            crate::websocket::auth::AuthErrorCode::TokenExpired,
1159                            "session policy version is stale; refresh the session token",
1160                        ));
1161                    }
1162                    Err(AccountPolicyError::ConflictingLimits { version }) => {
1163                        warn!(
1164                            account = %redact_identity(account),
1165                            version,
1166                            "signed account limits conflict for one policy version"
1167                        );
1168                        return Err(AuthDeny::new(
1169                            crate::websocket::auth::AuthErrorCode::InternalError,
1170                            "signed account limits conflict with previously observed policy",
1171                        ));
1172                    }
1173                    Err(AccountPolicyError::CapacityExhausted) => {
1174                        warn!("account policy state at capacity; denying admission");
1175                        return Err(AuthDeny::new(
1176                            crate::websocket::auth::AuthErrorCode::InternalError,
1177                            "account policy state is at capacity; retry shortly",
1178                        ));
1179                    }
1180                }
1181            }
1182
1183            // Check max connections per resolved consumer (token limits,
1184            // fallback to default limits). Legacy tokens resolve to the
1185            // subject, preserving old behavior.
1186            let max_connections = ctx.limits.max_connections.or_else(|| {
1187                self.rate_limit_config
1188                    .default_limits
1189                    .as_ref()
1190                    .and_then(|l| l.max_connections)
1191            });
1192            if let Some(max_connections) = max_connections {
1193                let current_connections = self.count_connections_for_consumer(ctx.consumer_key());
1194                if current_connections >= max_connections as usize {
1195                    return Err(AuthDeny::connection_limit_exceeded(
1196                        &format!("consumer {}", ctx.consumer_key()),
1197                        current_connections,
1198                        max_connections as usize,
1199                    ));
1200                }
1201            }
1202
1203            // Check aggregate concurrent connections per account from the
1204            // signed account limits.
1205            if !ctx.is_legacy_policy() && ctx.account_key.is_some() {
1206                if let Some(max_account_connections) = ctx.account_limits.max_connections {
1207                    let current_account_connections =
1208                        self.count_connections_for_account(ctx.account_key());
1209                    if current_account_connections >= max_account_connections as usize {
1210                        return Err(AuthDeny::connection_limit_exceeded(
1211                            &format!("account {}", ctx.account_key()),
1212                            current_account_connections,
1213                            max_account_connections as usize,
1214                        ));
1215                    }
1216                }
1217            }
1218
1219            // Check global max connections per metering key
1220            if let Some(max_per_metering_key) =
1221                self.rate_limit_config.max_connections_per_metering_key
1222            {
1223                let current_metering_connections =
1224                    self.count_connections_for_metering_key(&ctx.metering_key);
1225                if current_metering_connections >= max_per_metering_key {
1226                    return Err(AuthDeny::connection_limit_exceeded(
1227                        &format!("metering key {}", ctx.metering_key),
1228                        current_metering_connections,
1229                        max_per_metering_key,
1230                    ));
1231                }
1232            }
1233
1234            // Check global max connections per origin
1235            if let Some(max_per_origin) = self.rate_limit_config.max_connections_per_origin {
1236                if let Some(ref origin) = ctx.origin {
1237                    let current_origin_connections = self.count_connections_for_origin(origin);
1238                    if current_origin_connections >= max_per_origin {
1239                        return Err(AuthDeny::connection_limit_exceeded(
1240                            &format!("origin {}", origin),
1241                            current_origin_connections,
1242                            max_per_origin,
1243                        ));
1244                    }
1245                }
1246            }
1247        }
1248        Ok(())
1249    }
1250
1251    /// Count connections from a specific IP address
1252    fn count_connections_for_ip(&self, remote_addr: &SocketAddr) -> usize {
1253        let ip = remote_addr.ip();
1254        self.clients
1255            .iter()
1256            .filter(|entry| entry.value().remote_addr.ip() == ip)
1257            .count()
1258    }
1259
1260    /// Count connections for a resolved consumer identity
1261    fn count_connections_for_consumer(&self, consumer: &str) -> usize {
1262        self.clients
1263            .iter()
1264            .filter(|entry| {
1265                entry
1266                    .value()
1267                    .auth_context
1268                    .as_ref()
1269                    .map(|ctx| ctx.consumer_key() == consumer)
1270                    .unwrap_or(false)
1271            })
1272            .count()
1273    }
1274
1275    /// Count connections for a resolved account identity
1276    fn count_connections_for_account(&self, account: &str) -> usize {
1277        self.clients
1278            .iter()
1279            .filter(|entry| {
1280                entry
1281                    .value()
1282                    .auth_context
1283                    .as_ref()
1284                    .map(|ctx| ctx.account_key() == account)
1285                    .unwrap_or(false)
1286            })
1287            .count()
1288    }
1289
1290    /// Count connections for a specific metering key
1291    fn count_connections_for_metering_key(&self, metering_key: &str) -> usize {
1292        self.clients
1293            .iter()
1294            .filter(|entry| {
1295                entry
1296                    .value()
1297                    .auth_context
1298                    .as_ref()
1299                    .map(|ctx| ctx.metering_key == metering_key)
1300                    .unwrap_or(false)
1301            })
1302            .count()
1303    }
1304
1305    /// Count connections for a specific origin
1306    fn count_connections_for_origin(&self, origin: &str) -> usize {
1307        self.clients
1308            .iter()
1309            .filter(|entry| {
1310                entry
1311                    .value()
1312                    .auth_context
1313                    .as_ref()
1314                    .and_then(|ctx| ctx.origin.as_ref())
1315                    .map(|o| o == origin)
1316                    .unwrap_or(false)
1317            })
1318            .count()
1319    }
1320
1321    /// Check if a subscription is allowed for the given client.
1322    ///
1323    /// Returns Ok(()) if the subscription is allowed, or an error with a reason if not.
1324    #[allow(clippy::result_large_err)]
1325    pub async fn check_subscription_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
1326        let context = {
1327            let Some(client) = self.clients.get(&client_id) else {
1328                return Ok(());
1329            };
1330            let current_subs = client.subscription_count().await;
1331
1332            // Check max subscriptions per connection (use token limits, fallback to default limits)
1333            if let Some(ref ctx) = client.auth_context {
1334                let max_subs = ctx.limits.max_subscriptions.or_else(|| {
1335                    self.rate_limit_config
1336                        .default_limits
1337                        .as_ref()
1338                        .and_then(|l| l.max_subscriptions)
1339                });
1340                if let Some(max_subs) = max_subs {
1341                    if current_subs >= max_subs as usize {
1342                        return Err(AuthDeny::new(
1343                            crate::websocket::auth::AuthErrorCode::SubscriptionLimitExceeded,
1344                            format!(
1345                                "Subscription limit exceeded: {} of {} subscriptions for client {}",
1346                                current_subs, max_subs, client_id
1347                            ),
1348                        )
1349                        .with_suggested_action(
1350                            "Unsubscribe from an existing view before creating another subscription",
1351                        ));
1352                    }
1353                }
1354            }
1355            client.auth_context.clone()
1356        };
1357
1358        // Signed subscription-create rates, enforced only for v2 tokens that
1359        // carry the corresponding optional limit fields.
1360        if let (Some(rate_limiter), Some(ctx)) = (self.rate_limiter.as_ref(), context.as_ref()) {
1361            if !ctx.is_legacy_policy() {
1362                if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1363                    .check_subscription_create_for_consumer(
1364                        ctx.consumer_key(),
1365                        ctx.limits.max_subscription_creates_per_minute,
1366                    )
1367                    .await
1368                {
1369                    return Err(AuthDeny::rate_limited(retry_after, "subscription creates")
1370                        .with_context(format!(
1371                        "subscription-create rate limit for consumer {} of {} per minute exceeded",
1372                        ctx.consumer_key(),
1373                        limit
1374                    )));
1375                }
1376
1377                if ctx.account_key.is_some() {
1378                    if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1379                        .check_subscription_create_for_account(
1380                            ctx.account_key(),
1381                            ctx.account_limits.max_subscription_creates_per_minute,
1382                        )
1383                        .await
1384                    {
1385                        return Err(AuthDeny::rate_limited(
1386                            retry_after,
1387                            "account subscription creates",
1388                        )
1389                        .with_context(format!(
1390                            "subscription-create rate limit for account {} of {} per minute exceeded",
1391                            ctx.account_key(),
1392                            limit
1393                        )));
1394                    }
1395                }
1396            }
1397        }
1398        Ok(())
1399    }
1400
1401    /// Get metering key for a client
1402    pub fn get_metering_key(&self, client_id: Uuid) -> Option<String> {
1403        self.clients.get(&client_id).and_then(|client| {
1404            client
1405                .auth_context
1406                .as_ref()
1407                .map(|ctx| ctx.metering_key.clone())
1408        })
1409    }
1410
1411    /// Get auth context for a client.
1412    pub fn get_auth_context(&self, client_id: Uuid) -> Option<AuthContext> {
1413        self.clients
1414            .get(&client_id)
1415            .and_then(|client| client.auth_context.clone())
1416    }
1417
1418    /// Check if a snapshot request is allowed (based on max_snapshot_rows limit)
1419    ///
1420    /// Uses token limits if available, falls back to default limits from RateLimitConfig.
1421    #[allow(clippy::result_large_err)]
1422    pub fn check_snapshot_allowed(
1423        &self,
1424        client_id: Uuid,
1425        requested_rows: u32,
1426    ) -> Result<(), AuthDeny> {
1427        if let Some(client) = self.clients.get(&client_id) {
1428            if let Some(ref ctx) = client.auth_context {
1429                let max_rows = ctx.limits.max_snapshot_rows.or_else(|| {
1430                    self.rate_limit_config
1431                        .default_limits
1432                        .as_ref()
1433                        .and_then(|l| l.max_snapshot_rows)
1434                });
1435                if let Some(max_rows) = max_rows {
1436                    if requested_rows > max_rows {
1437                        return Err(AuthDeny::new(
1438                            crate::websocket::auth::AuthErrorCode::SnapshotLimitExceeded,
1439                            format!(
1440                                "Snapshot limit exceeded: requested {} rows, max allowed is {} for client {}",
1441                                requested_rows, max_rows, client_id
1442                            ),
1443                        )
1444                        .with_suggested_action(
1445                            "Request fewer rows or lower the snapshotLimit on the subscription",
1446                        ));
1447                    }
1448                }
1449            }
1450        }
1451        Ok(())
1452    }
1453}
1454
1455impl Default for ClientManager {
1456    fn default() -> Self {
1457        Self::new()
1458    }
1459}
1460
1461#[cfg(test)]
1462mod tests {
1463    use super::*;
1464    use crate::websocket::auth::AuthContext;
1465    use arete_auth::{KeyClass, Limits};
1466    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
1467
1468    fn create_test_auth_context(subject: &str, limits: Limits) -> AuthContext {
1469        AuthContext {
1470            subject: subject.to_string(),
1471            issuer: "test-issuer".to_string(),
1472            audience: "test-audience".to_string(),
1473            key_class: KeyClass::Publishable,
1474            metering_key: format!("meter-{}", subject),
1475            deployment_id: None,
1476            target_kind: None,
1477            target_id: None,
1478            program_id: None,
1479            program_release_hash: None,
1480            expires_at: u64::MAX,
1481            scope: "read".to_string(),
1482            limits,
1483            plan: None,
1484            origin: None,
1485            client_ip: None,
1486            jti: uuid::Uuid::new_v4().to_string(),
1487            actor_key: None,
1488            account_key: None,
1489            consumer_key: None,
1490            policy_version: None,
1491            account_limits: Limits::default(),
1492        }
1493    }
1494
1495    fn create_v2_auth_context(
1496        consumer: &str,
1497        account: &str,
1498        policy_version: u32,
1499        limits: Limits,
1500        account_limits: Limits,
1501    ) -> AuthContext {
1502        AuthContext {
1503            subject: "user:1".to_string(),
1504            issuer: "test-issuer".to_string(),
1505            audience: "test-audience".to_string(),
1506            key_class: KeyClass::Publishable,
1507            metering_key: account.to_string(),
1508            deployment_id: None,
1509            target_kind: None,
1510            target_id: None,
1511            program_id: None,
1512            program_release_hash: None,
1513            expires_at: u64::MAX,
1514            scope: "read".to_string(),
1515            limits,
1516            plan: Some("pro".to_string()),
1517            origin: None,
1518            client_ip: None,
1519            jti: uuid::Uuid::new_v4().to_string(),
1520            actor_key: Some("user:1".to_string()),
1521            account_key: Some(account.to_string()),
1522            consumer_key: Some(consumer.to_string()),
1523            policy_version: Some(policy_version),
1524            account_limits,
1525        }
1526    }
1527
1528    fn insert_client(manager: &ClientManager, context: AuthContext) -> Uuid {
1529        let (sender, receiver) = mpsc::channel(8);
1530        // Keep the receiver alive so the sender stays open.
1531        std::mem::forget(receiver);
1532        let client_id = Uuid::new_v4();
1533        manager.clients.insert(
1534            client_id,
1535            ClientInfo::new(
1536                client_id,
1537                sender,
1538                Some(context),
1539                create_test_socket_addr("127.0.0.1"),
1540            ),
1541        );
1542        client_id
1543    }
1544
1545    fn create_test_socket_addr(ip: &str) -> SocketAddr {
1546        SocketAddr::new(
1547            ip.parse::<IpAddr>()
1548                .unwrap_or(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1549            12345,
1550        )
1551    }
1552
1553    #[test]
1554    fn test_egress_tracker_basic() {
1555        let mut tracker = EgressTracker::new();
1556
1557        // Should allow bytes within limit
1558        assert!(tracker.record_bytes(500, 1000));
1559        assert_eq!(tracker.current_usage(), 500);
1560
1561        // Should allow more bytes within limit
1562        assert!(tracker.record_bytes(400, 1000));
1563        assert_eq!(tracker.current_usage(), 900);
1564
1565        // Should reject bytes over limit
1566        assert!(!tracker.record_bytes(200, 1000));
1567        assert_eq!(tracker.current_usage(), 900); // Usage shouldn't increase
1568    }
1569
1570    #[test]
1571    fn test_egress_tracker_window_reset() {
1572        let mut tracker = EgressTracker::new();
1573
1574        // Use up the limit
1575        assert!(tracker.record_bytes(100, 100));
1576        assert!(!tracker.record_bytes(1, 100));
1577
1578        // Reset the window
1579        tracker.bytes_this_minute = 0;
1580        tracker.window_start = SystemTime::now() - Duration::from_secs(61);
1581
1582        // Should allow after window reset
1583        assert!(tracker.record_bytes(50, 100));
1584    }
1585
1586    #[test]
1587    fn test_message_rate_tracker_basic() {
1588        let mut tracker = MessageRateTracker::new();
1589
1590        assert!(tracker.record_message(2));
1591        assert_eq!(tracker.current_usage(), 1);
1592
1593        assert!(tracker.record_message(2));
1594        assert_eq!(tracker.current_usage(), 2);
1595
1596        assert!(!tracker.record_message(2));
1597        assert_eq!(tracker.current_usage(), 2);
1598    }
1599
1600    #[tokio::test]
1601    async fn test_client_inbound_message_limit() {
1602        let (tx, _rx) = mpsc::channel(1);
1603        let client = ClientInfo::new(
1604            Uuid::new_v4(),
1605            tx,
1606            Some(create_test_auth_context(
1607                "user-1",
1608                Limits {
1609                    max_messages_per_minute: Some(2),
1610                    ..Default::default()
1611                },
1612            )),
1613            create_test_socket_addr("127.0.0.1"),
1614        );
1615
1616        assert_eq!(client.record_inbound_message(), Some(1));
1617        assert_eq!(client.record_inbound_message(), Some(2));
1618        assert_eq!(client.record_inbound_message(), None);
1619    }
1620
1621    #[tokio::test]
1622    async fn duplicate_subscription_id_is_rejected_without_replacement() {
1623        let (tx, _rx) = mpsc::channel(1);
1624        let client = ClientInfo::new(
1625            Uuid::new_v4(),
1626            tx,
1627            None,
1628            create_test_socket_addr("127.0.0.1"),
1629        );
1630        let first = CancellationToken::new();
1631        let duplicate = CancellationToken::new();
1632
1633        assert!(
1634            client
1635                .add_subscription("opaque-id".to_string(), first.clone())
1636                .await
1637        );
1638        assert!(
1639            !client
1640                .add_subscription("opaque-id".to_string(), duplicate.clone())
1641                .await
1642        );
1643        assert!(!first.is_cancelled());
1644        assert!(!duplicate.is_cancelled());
1645
1646        assert!(client.remove_subscription("opaque-id").await);
1647        assert!(first.is_cancelled());
1648        assert!(!duplicate.is_cancelled());
1649    }
1650
1651    #[tokio::test]
1652    async fn test_no_limits() {
1653        let manager = ClientManager::new();
1654        let addr = create_test_socket_addr("127.0.0.1");
1655
1656        // No auth context - should succeed
1657        assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1658
1659        // Auth context with no limits - should succeed
1660        let auth_context = create_test_auth_context("test", Limits::default());
1661        assert!(manager
1662            .check_connection_allowed(addr, &Some(auth_context))
1663            .await
1664            .is_ok());
1665    }
1666
1667    #[tokio::test]
1668    async fn test_per_subject_connection_limit() {
1669        let manager = ClientManager::new();
1670
1671        let limits = Limits {
1672            max_connections: Some(2),
1673            ..Default::default()
1674        };
1675
1676        let auth_context = create_test_auth_context("user-1", limits);
1677        let addr = create_test_socket_addr("127.0.0.1");
1678
1679        // First connection should succeed (no clients yet)
1680        assert!(manager
1681            .check_connection_allowed(addr, &Some(auth_context.clone()))
1682            .await
1683            .is_ok());
1684    }
1685
1686    #[tokio::test]
1687    async fn test_per_ip_connection_limit() {
1688        let manager = ClientManager::new().with_max_connections_per_ip(2);
1689        let addr = create_test_socket_addr("192.168.1.1");
1690
1691        // Should succeed when no connections from that IP
1692        assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1693    }
1694
1695    // Tests for RateLimitConfig
1696    #[test]
1697    fn rate_limit_config_default() {
1698        let config = RateLimitConfig::default();
1699        assert!(config.max_connections_per_ip.is_none());
1700        assert_eq!(config.client_timeout, Duration::from_secs(300));
1701        assert_eq!(config.message_queue_size, 512);
1702        assert!(config.max_reconnect_attempts.is_none());
1703        assert_eq!(config.message_rate_window, Duration::from_secs(60));
1704        assert_eq!(config.egress_rate_window, Duration::from_secs(60));
1705    }
1706
1707    #[test]
1708    fn rate_limit_config_builder_methods() {
1709        let config = RateLimitConfig::default()
1710            .with_max_connections_per_ip(10)
1711            .with_timeout(Duration::from_secs(600))
1712            .with_message_queue_size(1024)
1713            .with_rate_limit_window(Duration::from_secs(120));
1714
1715        assert_eq!(config.max_connections_per_ip, Some(10));
1716        assert_eq!(config.client_timeout, Duration::from_secs(600));
1717        assert_eq!(config.message_queue_size, 1024);
1718        assert_eq!(config.message_rate_window, Duration::from_secs(120));
1719        assert_eq!(config.egress_rate_window, Duration::from_secs(120));
1720    }
1721
1722    #[tokio::test]
1723    async fn client_manager_with_config() {
1724        let config = RateLimitConfig::default()
1725            .with_max_connections_per_ip(5)
1726            .with_timeout(Duration::from_secs(120))
1727            .with_message_queue_size(256);
1728
1729        let manager = ClientManager::with_config(config);
1730        let addr = create_test_socket_addr("10.0.0.1");
1731
1732        // Check that the configuration was applied
1733        assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(5));
1734        assert_eq!(
1735            manager.rate_limit_config().client_timeout,
1736            Duration::from_secs(120)
1737        );
1738        assert_eq!(manager.rate_limit_config().message_queue_size, 256);
1739
1740        // Should allow when under limit
1741        assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1742    }
1743
1744    #[tokio::test]
1745    async fn client_manager_builder_pattern() {
1746        let manager = ClientManager::new()
1747            .with_max_connections_per_ip(10)
1748            .with_timeout(Duration::from_secs(180))
1749            .with_message_queue_size(1024)
1750            .with_rate_limit_window(Duration::from_secs(90));
1751
1752        assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(10));
1753        assert_eq!(
1754            manager.rate_limit_config().client_timeout,
1755            Duration::from_secs(180)
1756        );
1757        assert_eq!(manager.rate_limit_config().message_queue_size, 1024);
1758        assert_eq!(
1759            manager.rate_limit_config().message_rate_window,
1760            Duration::from_secs(90)
1761        );
1762    }
1763
1764    // Integration test: Connection limits are enforced
1765    #[tokio::test]
1766    async fn connection_limit_enforcement_with_actual_clients() {
1767        let manager = ClientManager::new().with_max_connections_per_ip(2);
1768        let addr1 = create_test_socket_addr("192.168.1.1");
1769        let addr2 = create_test_socket_addr("192.168.1.2");
1770
1771        // First connection from IP1 should succeed
1772        let auth1 = create_test_auth_context("user-1", Limits::default());
1773        assert!(manager
1774            .check_connection_allowed(addr1, &Some(auth1.clone()))
1775            .await
1776            .is_ok());
1777
1778        // Simulate adding a client (we can't easily do this without a real WebSocket,
1779        // but we can verify the check logic works)
1780
1781        // Same IP, different auth context - should still count toward IP limit
1782        let auth2 = create_test_auth_context("user-2", Limits::default());
1783        assert!(manager
1784            .check_connection_allowed(addr1, &Some(auth2.clone()))
1785            .await
1786            .is_ok());
1787
1788        // Different IP - should succeed regardless
1789        let auth3 = create_test_auth_context("user-3", Limits::default());
1790        assert!(manager
1791            .check_connection_allowed(addr2, &Some(auth3.clone()))
1792            .await
1793            .is_ok());
1794    }
1795
1796    // Test subscription limit enforcement
1797    #[tokio::test]
1798    async fn subscription_limit_enforcement() {
1799        let manager = ClientManager::new();
1800        let addr = create_test_socket_addr("127.0.0.1");
1801
1802        // Create auth context with subscription limit
1803        let auth = create_test_auth_context(
1804            "user-1",
1805            Limits {
1806                max_subscriptions: Some(2),
1807                ..Default::default()
1808            },
1809        );
1810
1811        // Check should pass initially
1812        assert!(manager
1813            .check_connection_allowed(addr, &Some(auth.clone()))
1814            .await
1815            .is_ok());
1816
1817        // Note: We can't easily test the full subscription flow without a real connection,
1818        // but we verify the limit configuration is properly stored
1819        assert_eq!(auth.limits.max_subscriptions, Some(2));
1820    }
1821
1822    // Test snapshot limit enforcement
1823    #[tokio::test]
1824    async fn snapshot_limit_enforcement() {
1825        let manager = ClientManager::new();
1826        let addr = create_test_socket_addr("127.0.0.1");
1827
1828        let auth = create_test_auth_context(
1829            "user-1",
1830            Limits {
1831                max_snapshot_rows: Some(1000),
1832                ..Default::default()
1833            },
1834        );
1835
1836        assert!(manager
1837            .check_connection_allowed(addr, &Some(auth.clone()))
1838            .await
1839            .is_ok());
1840
1841        // Note: Actual snapshot limit checking happens in check_snapshot_allowed
1842        // which requires a connected client
1843    }
1844
1845    #[tokio::test]
1846    async fn two_consumers_share_the_account_connection_cap() {
1847        let manager = ClientManager::new();
1848        let account_limits = Limits {
1849            max_connections: Some(1),
1850            ..Limits::default()
1851        };
1852        let consumer_a = create_v2_auth_context(
1853            "consumer:a",
1854            "account:42",
1855            1,
1856            Limits::default(),
1857            account_limits.clone(),
1858        );
1859        insert_client(&manager, consumer_a);
1860
1861        // A different consumer on the same account is blocked by the
1862        // aggregate cap.
1863        let consumer_b = create_v2_auth_context(
1864            "consumer:b",
1865            "account:42",
1866            1,
1867            Limits::default(),
1868            account_limits.clone(),
1869        );
1870        let addr = create_test_socket_addr("127.0.0.1");
1871        assert!(manager
1872            .check_connection_allowed(addr, &Some(consumer_b))
1873            .await
1874            .is_err());
1875
1876        // A consumer on another account is unaffected.
1877        let other_account = create_v2_auth_context(
1878            "consumer:c",
1879            "account:43",
1880            1,
1881            Limits::default(),
1882            account_limits,
1883        );
1884        assert!(manager
1885            .check_connection_allowed(addr, &Some(other_account))
1886            .await
1887            .is_ok());
1888    }
1889
1890    #[tokio::test]
1891    async fn one_consumer_is_limited_independently_of_its_account() {
1892        let manager = ClientManager::new();
1893        let limits = Limits {
1894            max_connections: Some(1),
1895            ..Limits::default()
1896        };
1897        let account_limits = Limits {
1898            max_connections: Some(10),
1899            ..Limits::default()
1900        };
1901        let consumer_a = create_v2_auth_context(
1902            "consumer:a",
1903            "account:42",
1904            1,
1905            limits.clone(),
1906            account_limits.clone(),
1907        );
1908        insert_client(&manager, consumer_a.clone());
1909
1910        let addr = create_test_socket_addr("127.0.0.1");
1911        // The same consumer hits its own cap.
1912        assert!(manager
1913            .check_connection_allowed(addr, &Some(consumer_a))
1914            .await
1915            .is_err());
1916
1917        // A sibling consumer under the same account is still admitted.
1918        let consumer_b =
1919            create_v2_auth_context("consumer:b", "account:42", 1, limits, account_limits);
1920        assert!(manager
1921            .check_connection_allowed(addr, &Some(consumer_b))
1922            .await
1923            .is_ok());
1924    }
1925
1926    #[tokio::test]
1927    async fn policy_version_upgrade_stale_and_conflict_rules_apply() {
1928        let manager = ClientManager::new();
1929        let addr = create_test_socket_addr("127.0.0.1");
1930        let limits_v1 = Limits {
1931            max_connections: Some(5),
1932            ..Limits::default()
1933        };
1934        let limits_v2 = Limits {
1935            max_connections: Some(2),
1936            ..Limits::default()
1937        };
1938
1939        // Version 1 admits and creates state.
1940        let v1 = create_v2_auth_context(
1941            "consumer:a",
1942            "account:42",
1943            1,
1944            Limits::default(),
1945            limits_v1.clone(),
1946        );
1947        assert!(manager
1948            .check_connection_allowed(addr, &Some(v1.clone()))
1949            .await
1950            .is_ok());
1951
1952        // Version 2 with new limits replaces the policy.
1953        let v2 = create_v2_auth_context(
1954            "consumer:a",
1955            "account:42",
1956            2,
1957            Limits::default(),
1958            limits_v2.clone(),
1959        );
1960        assert!(manager
1961            .check_connection_allowed(addr, &Some(v2))
1962            .await
1963            .is_ok());
1964
1965        // A stale version-1 token is rejected once version 2 was observed.
1966        let stale = manager.check_connection_allowed(addr, &Some(v1)).await;
1967        assert!(stale.is_err());
1968
1969        // Version 2 with different limits is a signing/config fault.
1970        let conflict =
1971            create_v2_auth_context("consumer:a", "account:42", 2, Limits::default(), limits_v1);
1972        assert!(manager
1973            .check_connection_allowed(addr, &Some(conflict))
1974            .await
1975            .is_err());
1976    }
1977
1978    #[tokio::test]
1979    async fn legacy_tokens_keep_old_behavior_and_are_counted() {
1980        let manager = ClientManager::new();
1981        let addr = create_test_socket_addr("127.0.0.1");
1982        let legacy = create_test_auth_context(
1983            "user-1",
1984            Limits {
1985                max_connections: Some(2),
1986                ..Limits::default()
1987            },
1988        );
1989        assert!(legacy.is_legacy_policy());
1990
1991        assert!(manager
1992            .check_connection_allowed(addr, &Some(legacy.clone()))
1993            .await
1994            .is_ok());
1995        assert_eq!(manager.account_policies().legacy_token_count(), 1);
1996        // Legacy tokens create no account policy state.
1997        assert_eq!(manager.account_policies().tracked_accounts(), 0);
1998
1999        // Legacy connection counting still keys off the subject.
2000        insert_client(&manager, legacy.clone());
2001        insert_client(&manager, legacy.clone());
2002        assert!(manager
2003            .check_connection_allowed(addr, &Some(legacy))
2004            .await
2005            .is_err());
2006    }
2007
2008    #[tokio::test]
2009    async fn account_message_budget_is_shared_across_consumers() {
2010        let manager = ClientManager::new();
2011        let account_limits = Limits {
2012            max_messages_per_minute: Some(2),
2013            ..Limits::default()
2014        };
2015        let client_a = insert_client(
2016            &manager,
2017            create_v2_auth_context(
2018                "consumer:a",
2019                "account:42",
2020                1,
2021                Limits::default(),
2022                account_limits.clone(),
2023            ),
2024        );
2025        let client_b = insert_client(
2026            &manager,
2027            create_v2_auth_context(
2028                "consumer:b",
2029                "account:42",
2030                1,
2031                Limits::default(),
2032                account_limits,
2033            ),
2034        );
2035
2036        assert!(manager.check_inbound_message_allowed(client_a).is_ok());
2037        assert!(manager.check_inbound_message_allowed(client_b).is_ok());
2038        // The third message anywhere on the account is rejected.
2039        assert!(manager.check_inbound_message_allowed(client_a).is_err());
2040    }
2041
2042    #[tokio::test]
2043    async fn account_state_evicts_when_idle_and_unreferenced() {
2044        let manager = ClientManager::new();
2045        let addr = create_test_socket_addr("127.0.0.1");
2046        let context = create_v2_auth_context(
2047            "consumer:a",
2048            "account:42",
2049            1,
2050            Limits::default(),
2051            Limits::default(),
2052        );
2053        assert!(manager
2054            .check_connection_allowed(addr, &Some(context))
2055            .await
2056            .is_ok());
2057        assert_eq!(manager.account_policies().tracked_accounts(), 1);
2058
2059        // No live connection references the account; an idle sweep with a
2060        // zero TTL registry drops it.
2061        manager.account_policies().evict_idle(|_| false);
2062        assert_eq!(
2063            manager.account_policies().tracked_accounts(),
2064            1,
2065            "TTL keeps fresh entries"
2066        );
2067        manager.cleanup_account_state();
2068        assert_eq!(
2069            manager.account_policies().tracked_accounts(),
2070            1,
2071            "fresh entries survive sweep"
2072        );
2073    }
2074
2075    // Test WebSocketRateLimiter integration
2076    #[tokio::test]
2077    async fn test_rate_limiter_integration() {
2078        use crate::websocket::rate_limiter::{RateLimiterConfig, WebSocketRateLimiter};
2079
2080        let rate_limiter = Arc::new(WebSocketRateLimiter::new(RateLimiterConfig::default()));
2081        let manager = ClientManager::new().with_rate_limiter(rate_limiter);
2082        let addr = create_test_socket_addr("127.0.0.1");
2083
2084        // Should allow connections when rate limiter is configured
2085        let auth = create_test_auth_context("user-1", Limits::default());
2086        assert!(manager
2087            .check_connection_allowed(addr, &Some(auth))
2088            .await
2089            .is_ok());
2090    }
2091}