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