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    /// Cancelled when this client leaves the registry, however it leaves:
207    /// a disconnect, or an eviction for staleness, backpressure, token expiry
208    /// or a spent budget. The session and its subscription tokens hang off it.
209    connection_token: CancellationToken,
210}
211
212impl Drop for ClientInfo {
213    /// Leaving the registry ends the connection's work. Removal is the one
214    /// step every eviction path shares, so cancelling here means none of them
215    /// can leave a subscription task running for a client that is gone.
216    fn drop(&mut self) {
217        self.connection_token.cancel();
218        // Tokens a caller registered without deriving them from the
219        // connection token. The registry only writes this map while the
220        // client is registered, so an uncontended read sees every token.
221        match self.subscriptions.try_read() {
222            Ok(subscriptions) => subscriptions.values().for_each(CancellationToken::cancel),
223            Err(_) => {
224                let subscriptions = self.subscriptions.clone();
225                if let Ok(runtime) = tokio::runtime::Handle::try_current() {
226                    runtime.spawn(async move {
227                        subscriptions
228                            .read()
229                            .await
230                            .values()
231                            .for_each(CancellationToken::cancel);
232                    });
233                }
234            }
235        }
236    }
237}
238
239impl ClientInfo {
240    pub fn new(
241        id: Uuid,
242        sender: mpsc::Sender<Message>,
243        auth_context: Option<AuthContext>,
244        remote_addr: SocketAddr,
245    ) -> Self {
246        Self {
247            id,
248            last_seen: SystemTime::now(),
249            sender,
250            subscriptions: Arc::new(RwLock::new(HashMap::new())),
251            auth_context,
252            remote_addr,
253            egress_tracker: std::sync::Mutex::new(EgressTracker::new()),
254            message_rate_tracker: std::sync::Mutex::new(MessageRateTracker::new()),
255            close_frame: Arc::new(std::sync::OnceLock::new()),
256            admission_permit: None,
257            admission_lifecycle: Arc::new(AsyncMutex::new(())),
258            connection_token: CancellationToken::new(),
259        }
260    }
261
262    /// Record bytes sent, returning true if within limit
263    pub fn record_egress(&self, bytes: usize) -> Option<u64> {
264        if let Ok(mut tracker) = self.egress_tracker.lock() {
265            if let Some(ref ctx) = self.auth_context {
266                if let Some(limit) = ctx.limits.max_bytes_per_minute {
267                    if tracker.record_bytes(bytes, limit) {
268                        return Some(tracker.current_usage());
269                    } else {
270                        return None; // Limit exceeded
271                    }
272                }
273            }
274            // No limit set, return current usage (0)
275            return Some(tracker.current_usage());
276        }
277        None
278    }
279
280    /// Record an inbound client message, returning true if within limit.
281    pub fn record_inbound_message(&self) -> Option<u32> {
282        if let Ok(mut tracker) = self.message_rate_tracker.lock() {
283            if let Some(ref ctx) = self.auth_context {
284                if let Some(limit) = ctx.limits.max_messages_per_minute {
285                    if tracker.record_message(limit) {
286                        return Some(tracker.current_usage());
287                    } else {
288                        return None;
289                    }
290                }
291            }
292
293            return Some(tracker.current_usage());
294        }
295
296        None
297    }
298
299    pub fn update_last_seen(&mut self) {
300        self.last_seen = SystemTime::now();
301    }
302
303    pub fn is_stale(&self, timeout: Duration) -> bool {
304        self.last_seen.elapsed().unwrap_or(Duration::MAX) > timeout
305    }
306
307    pub async fn add_subscription(
308        &self,
309        subscription_id: String,
310        token: CancellationToken,
311    ) -> bool {
312        let _lifecycle = self.admission_lifecycle.lock().await;
313        let mut subs = self.subscriptions.write().await;
314        match subs.entry(subscription_id) {
315            std::collections::hash_map::Entry::Vacant(entry) => {
316                entry.insert(token);
317                true
318            }
319            std::collections::hash_map::Entry::Occupied(_) => false,
320        }
321    }
322
323    pub async fn remove_subscription(&self, subscription_id: &str) -> bool {
324        let _lifecycle = self.admission_lifecycle.lock().await;
325        let mut subs = self.subscriptions.write().await;
326        if let Some(token) = subs.remove(subscription_id) {
327            token.cancel();
328            debug!("Cancelled subscription: {}", subscription_id);
329            true
330        } else {
331            debug!(
332                "Subscription not found for cancellation: {}",
333                subscription_id
334            );
335            false
336        }
337    }
338
339    pub async fn cancel_all_subscriptions(&self) {
340        let _lifecycle = self.admission_lifecycle.lock().await;
341        let subs = self.subscriptions.read().await;
342        for (subscription_id, token) in subs.iter() {
343            token.cancel();
344            debug!("Cancelled subscription on disconnect: {}", subscription_id);
345        }
346    }
347
348    pub async fn subscription_count(&self) -> usize {
349        self.subscriptions.read().await.len()
350    }
351}
352
353/// Configuration for rate limiting in ClientManager
354///
355/// These settings control various rate limits at the connection level.
356/// Per-subject limits are controlled via AuthContext.Limits.
357#[derive(Debug, Clone)]
358pub struct RateLimitConfig {
359    /// Global maximum connections per IP address
360    pub max_connections_per_ip: Option<usize>,
361    /// Global maximum connections per metering key
362    pub max_connections_per_metering_key: Option<usize>,
363    /// Global maximum connections per origin
364    pub max_connections_per_origin: Option<usize>,
365    /// Default connection timeout for stale client cleanup
366    pub client_timeout: Duration,
367    /// Message queue size per client
368    pub message_queue_size: usize,
369    /// Maximum reconnect attempts per client (optional global default)
370    pub max_reconnect_attempts: Option<u32>,
371    /// Rate limit window duration for message counting
372    pub message_rate_window: Duration,
373    /// Rate limit window duration for egress tracking
374    pub egress_rate_window: Duration,
375    /// Default limits applied when auth token doesn't specify limits
376    /// These act as server-wide fallback limits for all connections
377    pub default_limits: Option<Limits>,
378}
379
380impl Default for RateLimitConfig {
381    fn default() -> Self {
382        Self {
383            max_connections_per_ip: None,
384            max_connections_per_metering_key: None,
385            max_connections_per_origin: None,
386            client_timeout: Duration::from_secs(300),
387            message_queue_size: 512,
388            max_reconnect_attempts: None,
389            message_rate_window: Duration::from_secs(60),
390            egress_rate_window: Duration::from_secs(60),
391            default_limits: None,
392        }
393    }
394}
395
396impl RateLimitConfig {
397    /// Load configuration from environment variables
398    ///
399    /// Environment variables:
400    /// - `ARETE_WS_MAX_CONNECTIONS_PER_IP` - Max connections per IP (default: unlimited)
401    /// - `ARETE_WS_MAX_CONNECTIONS_PER_METERING_KEY` - Max connections per metering key (default: unlimited)
402    /// - `ARETE_WS_MAX_CONNECTIONS_PER_ORIGIN` - Max connections per origin (default: unlimited)
403    /// - `ARETE_WS_CLIENT_TIMEOUT_SECS` - Client timeout in seconds (default: 300)
404    /// - `ARETE_WS_MESSAGE_QUEUE_SIZE` - Message queue size per client (default: 512)
405    /// - `ARETE_WS_RATE_LIMIT_WINDOW_SECS` - Rate limit window in seconds (default: 60)
406    /// - `ARETE_WS_DEFAULT_MAX_CONNECTIONS` - Default max connections per subject (fallback when token has no limit)
407    /// - `ARETE_WS_DEFAULT_MAX_SUBSCRIPTIONS` - Default max subscriptions per connection (fallback when token has no limit)
408    /// - `ARETE_WS_DEFAULT_MAX_SNAPSHOT_ROWS` - Default max snapshot rows per request (fallback when token has no limit)
409    /// - `ARETE_WS_DEFAULT_MAX_MESSAGES_PER_MINUTE` - Default max messages per minute (fallback when token has no limit)
410    /// - `ARETE_WS_DEFAULT_MAX_BYTES_PER_MINUTE` - Default max bytes per minute (fallback when token has no limit)
411    pub fn from_env() -> Self {
412        let mut config = Self::default();
413
414        if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_IP") {
415            if let Ok(max) = val.parse() {
416                config.max_connections_per_ip = Some(max);
417            }
418        }
419
420        if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_METERING_KEY") {
421            if let Ok(max) = val.parse() {
422                config.max_connections_per_metering_key = Some(max);
423            }
424        }
425
426        if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_ORIGIN") {
427            if let Ok(max) = val.parse() {
428                config.max_connections_per_origin = Some(max);
429            }
430        }
431
432        if let Ok(val) = std::env::var("ARETE_WS_CLIENT_TIMEOUT_SECS") {
433            if let Ok(secs) = val.parse() {
434                config.client_timeout = Duration::from_secs(secs);
435            }
436        }
437
438        if let Ok(val) = std::env::var("ARETE_WS_MESSAGE_QUEUE_SIZE") {
439            if let Ok(size) = val.parse() {
440                config.message_queue_size = size;
441            }
442        }
443
444        if let Ok(val) = std::env::var("ARETE_WS_RATE_LIMIT_WINDOW_SECS") {
445            if let Ok(secs) = val.parse() {
446                config.message_rate_window = Duration::from_secs(secs);
447                config.egress_rate_window = Duration::from_secs(secs);
448            }
449        }
450
451        // Load default limits from environment (fallback when auth token doesn't specify limits)
452        let mut default_limits = Limits::default();
453        let mut has_default_limits = false;
454
455        if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_CONNECTIONS") {
456            if let Ok(max) = val.parse() {
457                default_limits.max_connections = Some(max);
458                has_default_limits = true;
459            }
460        }
461
462        if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_SUBSCRIPTIONS") {
463            if let Ok(max) = val.parse() {
464                default_limits.max_subscriptions = Some(max);
465                has_default_limits = true;
466            }
467        }
468
469        if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_SNAPSHOT_ROWS") {
470            if let Ok(max) = val.parse() {
471                default_limits.max_snapshot_rows = Some(max);
472                has_default_limits = true;
473            }
474        }
475
476        if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_MESSAGES_PER_MINUTE") {
477            if let Ok(max) = val.parse() {
478                default_limits.max_messages_per_minute = Some(max);
479                has_default_limits = true;
480            }
481        }
482
483        if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_BYTES_PER_MINUTE") {
484            if let Ok(max) = val.parse() {
485                default_limits.max_bytes_per_minute = Some(max);
486                has_default_limits = true;
487            }
488        }
489
490        if has_default_limits {
491            config.default_limits = Some(default_limits);
492        }
493
494        config
495    }
496
497    /// Set the maximum connections per IP
498    pub fn with_max_connections_per_ip(mut self, max: usize) -> Self {
499        self.max_connections_per_ip = Some(max);
500        self
501    }
502
503    /// Set the client timeout
504    pub fn with_timeout(mut self, timeout: Duration) -> Self {
505        self.client_timeout = timeout;
506        self
507    }
508
509    /// Set the message queue size
510    pub fn with_message_queue_size(mut self, size: usize) -> Self {
511        self.message_queue_size = size;
512        self
513    }
514
515    /// Set the rate limit window (applies to both message and egress windows)
516    pub fn with_rate_limit_window(mut self, window: Duration) -> Self {
517        self.message_rate_window = window;
518        self.egress_rate_window = window;
519        self
520    }
521
522    /// Set default limits applied when auth token doesn't specify limits
523    ///
524    /// These limits act as server-wide fallbacks for connections
525    /// where the authentication token doesn't include explicit limits.
526    pub fn with_default_limits(mut self, limits: Limits) -> Self {
527        self.default_limits = Some(limits);
528        self
529    }
530}
531
532/// Bound on tracked account usage entries; signed account keys keep the
533/// cardinality legitimate, this is a hard backstop.
534const MAX_TRACKED_ACCOUNT_USAGE: usize = crate::account_policy::DEFAULT_MAX_TRACKED_ACCOUNTS;
535
536/// Idle TTL after which aggregate account usage state is evicted.
537const ACCOUNT_USAGE_IDLE_TTL: Duration = crate::account_policy::DEFAULT_ACCOUNT_IDLE_TTL;
538
539/// How long a client that has left the registry has to take its remaining
540/// queued frames and its close frame before the server drops its socket.
541#[cfg(not(test))]
542const REMOVED_CLIENT_WRITE_GRACE: Duration = Duration::from_secs(5);
543#[cfg(test)]
544const REMOVED_CLIENT_WRITE_GRACE: Duration = Duration::from_millis(500);
545
546/// Manages all connected WebSocket clients using lock-free DashMap.
547///
548/// Key design decisions:
549/// - Uses DashMap for lock-free concurrent access to client registry
550/// - Uses try_send instead of send to never block on slow clients
551/// - Disconnects clients that are backpressured (queue full) to prevent cascade failures
552/// - All public methods are non-blocking or use fine-grained per-key locks
553/// - Supports configurable rate limiting per IP, subject, and global defaults
554#[derive(Clone)]
555pub struct ClientManager {
556    clients: Arc<DashMap<Uuid, ClientInfo>>,
557    rate_limit_config: RateLimitConfig,
558    /// Optional WebSocket rate limiter for granular rate control
559    rate_limiter: Option<Arc<WebSocketRateLimiter>>,
560    /// Per-process account policy versions and signed aggregate limits
561    account_policies: Arc<AccountPolicyRegistry>,
562    /// Per-process aggregate message/egress usage per account
563    account_usage: Arc<DashMap<String, AccountUsage>>,
564    /// Optional embedder-owned admission state shared across runtimes.
565    admission_provider: Option<Arc<dyn WebSocketAdmissionProvider>>,
566    /// Held from an in-band refresh's identity checks until its new context
567    /// is installed, so concurrent refreshes see each other's identities.
568    refresh_admission: Arc<AsyncMutex<()>>,
569}
570
571impl ClientManager {
572    pub fn new() -> Self {
573        Self::with_config(RateLimitConfig::default())
574    }
575
576    /// Create a new ClientManager with the given rate limit configuration
577    pub fn with_config(config: RateLimitConfig) -> Self {
578        Self {
579            clients: Arc::new(DashMap::new()),
580            rate_limit_config: config,
581            rate_limiter: None,
582            account_policies: Arc::new(AccountPolicyRegistry::default()),
583            account_usage: Arc::new(DashMap::new()),
584            admission_provider: None,
585            refresh_admission: Arc::new(AsyncMutex::new(())),
586        }
587    }
588
589    /// Install an embedder-owned provider for cross-runtime admission.
590    pub fn with_admission_provider(
591        mut self,
592        provider: Arc<dyn WebSocketAdmissionProvider>,
593    ) -> Self {
594        self.admission_provider = Some(provider);
595        self
596    }
597
598    #[allow(clippy::result_large_err)]
599    pub(crate) fn reserve_connection_admission(
600        &self,
601        context: &AuthContext,
602    ) -> Result<Option<Arc<dyn WebSocketConnectionPermit>>, AuthDeny> {
603        self.admission_provider
604            .as_ref()
605            .map(|provider| provider.reserve_connection(context))
606            .unwrap_or(Ok(None))
607    }
608
609    /// Load configuration from environment variables
610    ///
611    /// See `RateLimitConfig::from_env` for supported variables.
612    pub fn from_env() -> Self {
613        Self::with_config(RateLimitConfig::from_env())
614    }
615
616    /// Set the client timeout for stale client cleanup
617    pub fn with_timeout(mut self, timeout: Duration) -> Self {
618        self.rate_limit_config.client_timeout = timeout;
619        self
620    }
621
622    /// Set the message queue size per client
623    pub fn with_message_queue_size(mut self, queue_size: usize) -> Self {
624        self.rate_limit_config.message_queue_size = queue_size;
625        self
626    }
627
628    /// Set a global limit on connections per IP address
629    pub fn with_max_connections_per_ip(mut self, max: usize) -> Self {
630        self.rate_limit_config.max_connections_per_ip = Some(max);
631        self
632    }
633
634    /// Set the rate limit window duration
635    pub fn with_rate_limit_window(mut self, window: Duration) -> Self {
636        self.rate_limit_config.message_rate_window = window;
637        self.rate_limit_config.egress_rate_window = window;
638        self
639    }
640
641    /// Set default limits applied when auth token doesn't specify limits
642    ///
643    /// These limits act as server-wide fallbacks for connections
644    /// where the authentication token doesn't include explicit limits.
645    pub fn with_default_limits(mut self, limits: Limits) -> Self {
646        self.rate_limit_config.default_limits = Some(limits);
647        self
648    }
649
650    /// Set a WebSocket rate limiter for granular rate control
651    pub fn with_rate_limiter(mut self, rate_limiter: Arc<WebSocketRateLimiter>) -> Self {
652        self.rate_limiter = Some(rate_limiter);
653        self
654    }
655
656    /// Get the rate limiter if configured
657    pub fn rate_limiter(&self) -> Option<&WebSocketRateLimiter> {
658        self.rate_limiter.as_ref().map(|r| r.as_ref())
659    }
660
661    /// Get the current rate limit configuration
662    pub fn rate_limit_config(&self) -> &RateLimitConfig {
663        &self.rate_limit_config
664    }
665
666    /// Account policy state observed by this manager.
667    pub fn account_policies(&self) -> &AccountPolicyRegistry {
668        &self.account_policies
669    }
670
671    /// Record aggregate account egress; true when within the signed limit.
672    fn record_account_egress(&self, ctx: &AuthContext, bytes: usize) -> bool {
673        if ctx.is_legacy_policy() || ctx.account_key.is_none() {
674            return true;
675        }
676        let Some(limit) = ctx.account_limits.max_bytes_per_minute else {
677            return true;
678        };
679        let account = ctx.account_key();
680        if let Some(usage) = self.account_usage.get(account) {
681            return usage.record_egress(bytes, limit);
682        }
683        if self.account_usage.len() >= MAX_TRACKED_ACCOUNT_USAGE {
684            warn!(
685                account = %redact_identity(account),
686                "account usage tracking at capacity; aggregate egress guard skipped"
687            );
688            return true;
689        }
690        self.account_usage
691            .entry(account.to_string())
692            .or_insert_with(AccountUsage::new)
693            .record_egress(bytes, limit)
694    }
695
696    /// Record one aggregate account inbound message; true when within limit.
697    fn record_account_message(&self, ctx: &AuthContext) -> bool {
698        if ctx.is_legacy_policy() || ctx.account_key.is_none() {
699            return true;
700        }
701        let Some(limit) = ctx.account_limits.max_messages_per_minute else {
702            return true;
703        };
704        let account = ctx.account_key();
705        if let Some(usage) = self.account_usage.get(account) {
706            return usage.record_message(limit);
707        }
708        if self.account_usage.len() >= MAX_TRACKED_ACCOUNT_USAGE {
709            warn!(
710                account = %redact_identity(account),
711                "account usage tracking at capacity; aggregate message guard skipped"
712            );
713            return true;
714        }
715        self.account_usage
716            .entry(account.to_string())
717            .or_insert_with(AccountUsage::new)
718            .record_message(limit)
719    }
720
721    /// Enforce the per-connection and aggregate account egress budgets.
722    fn enforce_egress_budgets(&self, client_id: Uuid, bytes: usize) -> Result<(), SendError> {
723        let (connection_ok, account_ok, permit_id) = {
724            let Some(client) = self.clients.get(&client_id) else {
725                return Err(SendError::ClientNotFound);
726            };
727            let connection_ok = client.record_egress(bytes).is_some();
728            let account_ok = !connection_ok
729                || client
730                    .auth_context
731                    .as_ref()
732                    .map(|ctx| self.record_account_egress(ctx, bytes))
733                    .unwrap_or(true);
734            let permit_id = client.admission_permit.as_ref().map(|permit| permit.id());
735            (connection_ok, account_ok, permit_id)
736        };
737        if !connection_ok {
738            warn!("Client {} exceeded egress limit, disconnecting", client_id);
739            self.clients.remove(&client_id);
740            return Err(SendError::ClientDisconnected);
741        }
742        if !account_ok {
743            warn!(
744                "Client {} exceeded the account egress limit, disconnecting",
745                client_id
746            );
747            self.clients.remove(&client_id);
748            return Err(SendError::ClientDisconnected);
749        }
750        if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
751            if let Err(deny) = provider.check_egress(permit_id, bytes) {
752                warn!(client = %client_id, reason = %deny.reason, "admission provider denied egress");
753                self.clients.remove(&client_id);
754                return Err(SendError::ClientDisconnected);
755            }
756        }
757        Ok(())
758    }
759
760    /// Add a new client connection.
761    ///
762    /// Spawns a dedicated sender task for this client that reads from its mpsc channel
763    /// and writes to the WebSocket. If the WebSocket write fails, the client is automatically
764    /// removed from the registry.
765    #[allow(clippy::result_large_err)]
766    pub fn add_client(
767        &self,
768        client_id: Uuid,
769        ws_sender: WebSocketSender,
770        auth_context: Option<AuthContext>,
771        remote_addr: SocketAddr,
772    ) -> Result<(), AuthDeny> {
773        let permit = if let Some(context) = auth_context.as_ref() {
774            self.reserve_connection_admission(context)?
775        } else {
776            None
777        };
778        self.add_client_with_admission(client_id, ws_sender, auth_context, remote_addr, permit);
779        Ok(())
780    }
781
782    pub(crate) fn add_client_with_admission(
783        &self,
784        client_id: Uuid,
785        mut ws_sender: WebSocketSender,
786        auth_context: Option<AuthContext>,
787        remote_addr: SocketAddr,
788        admission_permit: Option<Arc<dyn WebSocketConnectionPermit>>,
789    ) {
790        let (client_tx, mut client_rx) =
791            mpsc::channel::<Message>(self.rate_limit_config.message_queue_size);
792        let mut client_info = ClientInfo::new(client_id, client_tx, auth_context, remote_addr);
793        client_info.admission_permit = admission_permit;
794        let close_frame = client_info.close_frame.clone();
795
796        let clients_ref = self.clients.clone();
797        let connection_token = client_info.connection_token.clone();
798        tokio::spawn(async move {
799            let deliver = async {
800                while let Some(message) = client_rx.recv().await {
801                    if let Err(e) = ws_sender.send(message).await {
802                        warn!("Failed to send message to client {}: {}", client_id, e);
803                        break;
804                    }
805                }
806                // Removing a client closes its queue. Complete the WebSocket
807                // close handshake as well so receivers do not wait forever on
808                // a socket whose server-side delivery task has already
809                // stopped, and say why when the server dropped the client on
810                // purpose.
811                if let Some(frame) = close_frame.get() {
812                    let _ = ws_sender.send(Message::Close(Some(frame.clone()))).await;
813                }
814                let _ = ws_sender.close().await;
815            };
816            tokio::pin!(deliver);
817            // Once the client has left the registry, what is still queued and
818            // the close frame get a bounded grace. A peer that stops reading
819            // would otherwise block a write forever, and this task would keep
820            // a socket open that no connection cap counts any more.
821            tokio::select! {
822                () = &mut deliver => {}
823                () = connection_token.cancelled() => {
824                    if tokio::time::timeout(REMOVED_CLIENT_WRITE_GRACE, &mut deliver)
825                        .await
826                        .is_err()
827                    {
828                        warn!(
829                            "Client {} did not take its remaining frames in time; dropping its socket",
830                            client_id
831                        );
832                    }
833                }
834            }
835            clients_ref.remove(&client_id);
836            debug!("WebSocket sender task for client {} stopped", client_id);
837        });
838
839        self.clients.insert(client_id, client_info);
840        info!("Client {} registered from {}", client_id, remote_addr);
841    }
842
843    /// Remove a client from the registry.
844    pub fn remove_client(&self, client_id: Uuid) {
845        if self.clients.remove(&client_id).is_some() {
846            info!("Client {} removed", client_id);
847        }
848    }
849
850    /// Update the auth context for a client.
851    ///
852    /// Used for in-band auth refresh without reconnecting.
853    pub fn update_client_auth(&self, client_id: Uuid, auth_context: AuthContext) -> bool {
854        self.try_update_client_auth(client_id, auth_context)
855            .unwrap_or(false)
856    }
857
858    /// Attempt a synchronous auth refresh without waiting for an in-flight
859    /// subscription admission change.
860    ///
861    /// Unlike [`Self::update_client_auth`], this preserves the reason a
862    /// refresh was not applied. Embedding hosts can retry an
863    /// [`AuthErrorCode::InternalError`] whose reason is `Subscription
864    /// admission is busy`, or use [`Self::update_client_auth_async`] when an
865    /// async call site can wait for the lifecycle lock.
866    #[allow(clippy::result_large_err)]
867    pub fn try_update_client_auth(
868        &self,
869        client_id: Uuid,
870        auth_context: AuthContext,
871    ) -> Result<bool, AuthDeny> {
872        let Some(lifecycle) = self
873            .clients
874            .get(&client_id)
875            .map(|client| client.admission_lifecycle.clone())
876        else {
877            return Ok(false);
878        };
879        let _lifecycle = lifecycle.try_lock().map_err(|_| {
880            AuthDeny::new(
881                AuthErrorCode::InternalError,
882                "Subscription admission is busy",
883            )
884        })?;
885
886        let (subscriptions, current_permit) = {
887            let Some(client) = self.clients.get(&client_id) else {
888                return Ok(false);
889            };
890            if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
891                return Ok(false);
892            }
893            (
894                client.subscriptions.clone(),
895                client.admission_permit.clone(),
896            )
897        };
898        let subscriptions = subscriptions.try_read().map_err(|_| {
899            AuthDeny::new(
900                AuthErrorCode::InternalError,
901                "Subscription admission is busy",
902            )
903        })?;
904        let active_subscriptions = subscriptions.keys().cloned().collect::<Vec<_>>();
905        drop(subscriptions);
906        // Serialize the cap check with the identity change across sockets, so
907        // two refreshes cannot both take an identity's last place.
908        let _refresh_admission = self.refresh_admission.try_lock().map_err(|_| {
909            AuthDeny::new(
910                AuthErrorCode::InternalError,
911                "Subscription admission is busy",
912            )
913        })?;
914        self.check_refresh_admission(client_id, &auth_context, active_subscriptions.len())?;
915
916        let permit = match &self.admission_provider {
917            Some(provider) => {
918                provider.refresh_connection(current_permit, &auth_context, &active_subscriptions)?
919            }
920            None => current_permit,
921        };
922
923        let Some(mut client) = self.clients.get_mut(&client_id) else {
924            return Ok(false);
925        };
926        if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
927            return Ok(false);
928        }
929        client.admission_permit = permit;
930        client.auth_context = Some(auth_context);
931        debug!("Updated auth context for client {}", client_id);
932        Ok(true)
933    }
934
935    /// Admit an in-band refresh as the handshake would admit its token.
936    ///
937    /// A refresh installs a new identity and new limits on a live socket, so
938    /// the token must pass the same identity checks as a new connection, and
939    /// the socket's open subscriptions must fit the token's subscription cap.
940    /// A refusal leaves the current context in place.
941    #[allow(clippy::result_large_err)]
942    fn check_refresh_admission(
943        &self,
944        client_id: Uuid,
945        auth_context: &AuthContext,
946        active_subscriptions: usize,
947    ) -> Result<(), AuthDeny> {
948        let publishable = auth_context.key_class == arete_auth::KeyClass::Publishable;
949        self.check_identity_limits(auth_context, Some(client_id))
950            .and_then(|()| {
951                let max_subs = auth_context.limits.max_subscriptions.or_else(|| {
952                    self.rate_limit_config
953                        .default_limits
954                        .as_ref()
955                        .and_then(|limits| limits.max_subscriptions)
956                });
957                match max_subs {
958                    Some(max_subs) if active_subscriptions > max_subs as usize => {
959                        Err(AuthDeny::new(
960                            AuthErrorCode::SubscriptionLimitExceeded,
961                            format!(
962                                "Subscription limit exceeded: {} of {} subscriptions for client {}",
963                                active_subscriptions, max_subs, client_id
964                            ),
965                        )
966                        .with_suggested_action(
967                            "Unsubscribe from views before refreshing to this token",
968                        ))
969                    }
970                    _ => Ok(()),
971                }
972            })
973            .map_err(|deny| {
974                if publishable {
975                    deny.for_publishable_session()
976                } else {
977                    deny
978                }
979            })
980    }
981
982    /// Wait for an in-flight subscription admission change before refreshing
983    /// authentication. The synchronous [`Self::update_client_auth`] remains
984    /// available for embedding hosts that need its original non-blocking API.
985    pub async fn update_client_auth_async(
986        &self,
987        client_id: Uuid,
988        auth_context: AuthContext,
989    ) -> bool {
990        self.try_update_client_auth_async(client_id, auth_context)
991            .await
992            .unwrap_or(false)
993    }
994
995    #[allow(clippy::result_large_err)]
996    pub(crate) async fn try_update_client_auth_async(
997        &self,
998        client_id: Uuid,
999        auth_context: AuthContext,
1000    ) -> Result<bool, AuthDeny> {
1001        let Some(lifecycle) = self
1002            .clients
1003            .get(&client_id)
1004            .map(|client| client.admission_lifecycle.clone())
1005        else {
1006            return Ok(false);
1007        };
1008        let _lifecycle = lifecycle.lock().await;
1009
1010        let (subscriptions, current_permit) = {
1011            let Some(client) = self.clients.get(&client_id) else {
1012                return Ok(false);
1013            };
1014            if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
1015                return Ok(false);
1016            }
1017            (
1018                client.subscriptions.clone(),
1019                client.admission_permit.clone(),
1020            )
1021        };
1022        let subscriptions = subscriptions.read().await;
1023        let active_subscriptions = subscriptions.keys().cloned().collect::<Vec<_>>();
1024        drop(subscriptions);
1025        // Serialize the cap check with the identity change across sockets, so
1026        // two refreshes cannot both take an identity's last place.
1027        let _refresh_admission = self.refresh_admission.lock().await;
1028        self.check_refresh_admission(client_id, &auth_context, active_subscriptions.len())?;
1029
1030        let permit = match &self.admission_provider {
1031            Some(provider) => {
1032                provider.refresh_connection(current_permit, &auth_context, &active_subscriptions)?
1033            }
1034            None => current_permit,
1035        };
1036
1037        let Some(mut client) = self.clients.get_mut(&client_id) else {
1038            return Ok(false);
1039        };
1040        if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
1041            return Ok(false);
1042        }
1043        client.admission_permit = permit;
1044        client.auth_context = Some(auth_context);
1045        debug!("Updated auth context for client {}", client_id);
1046        Ok(true)
1047    }
1048
1049    /// Check if a client's token has expired.
1050    ///
1051    /// Returns true if the client has an auth context and it has expired.
1052    /// If expired, the client is removed from the registry.
1053    pub fn check_and_remove_expired(&self, client_id: Uuid) -> bool {
1054        let now = std::time::SystemTime::now()
1055            .duration_since(std::time::UNIX_EPOCH)
1056            .unwrap_or_default()
1057            .as_secs();
1058        // Check and remove under one shard lock. Calling `remove` while a
1059        // `get` guard on the same key is alive deadlocks this thread, and
1060        // every later caller that touches the shard queues behind it.
1061        let Some((_, client)) = self.clients.remove_if(&client_id, |_, client| {
1062            client
1063                .auth_context
1064                .as_ref()
1065                .is_some_and(|ctx| ctx.expires_at <= now)
1066        }) else {
1067            return false;
1068        };
1069        if let Some(ctx) = &client.auth_context {
1070            warn!(
1071                "Client {} token expired (expired at {}), disconnecting",
1072                client_id, ctx.expires_at
1073            );
1074        }
1075        // Say why the socket closes. The sender task sends this after the
1076        // queue drains, so it is the last frame the client sees; the SDKs read
1077        // a `token-expired:` reason as "mint a new token and reconnect".
1078        let _ = client.close_frame.set(CloseFrame {
1079            code: CloseCode::Policy,
1080            reason: format!(
1081                "{}: Authentication token expired",
1082                AuthErrorCode::TokenExpired.as_str()
1083            )
1084            .into(),
1085        });
1086        true
1087    }
1088
1089    /// Get the current number of connected clients.
1090    ///
1091    /// This is lock-free and returns an approximate count (may be slightly stale
1092    /// under high concurrency, which is fine for max_clients checks).
1093    pub fn client_count(&self) -> usize {
1094        self.clients.len()
1095    }
1096
1097    /// Send data to a specific client (non-blocking).
1098    ///
1099    /// This method NEVER blocks. If the client's queue is full, the client is
1100    /// considered too slow and is disconnected to prevent cascade failures.
1101    /// Use this for live streaming updates.
1102    ///
1103    /// For initial snapshots where you expect to send many messages at once,
1104    /// use `send_to_client_async` instead which will wait for queue space.
1105    pub fn send_to_client(&self, client_id: Uuid, data: Arc<Bytes>) -> Result<(), SendError> {
1106        // Check if client token has expired before sending
1107        if self.check_and_remove_expired(client_id) {
1108            return Err(SendError::ClientDisconnected);
1109        }
1110
1111        // Check per-connection and aggregate account egress limits
1112        self.enforce_egress_budgets(client_id, data.len())?;
1113
1114        let sender = {
1115            let client = self
1116                .clients
1117                .get(&client_id)
1118                .ok_or(SendError::ClientNotFound)?;
1119            client.sender.clone()
1120        };
1121
1122        let msg = Message::Binary((*data).clone());
1123        match sender.try_send(msg) {
1124            Ok(()) => Ok(()),
1125            Err(mpsc::error::TrySendError::Full(_)) => {
1126                warn!(
1127                    "Client {} backpressured (queue full), disconnecting",
1128                    client_id
1129                );
1130                self.clients.remove(&client_id);
1131                Err(SendError::ClientBackpressured)
1132            }
1133            Err(mpsc::error::TrySendError::Closed(_)) => {
1134                debug!("Client {} channel closed", client_id);
1135                self.clients.remove(&client_id);
1136                Err(SendError::ClientDisconnected)
1137            }
1138        }
1139    }
1140
1141    /// Send data to a specific client (async, waits for queue space).
1142    ///
1143    /// This method will wait if the client's queue is full, allowing the client
1144    /// time to catch up. Use this for initial snapshots where you need to send
1145    /// many messages at once.
1146    ///
1147    /// For live streaming updates, use `send_to_client` instead which will
1148    /// disconnect slow clients rather than blocking.
1149    pub async fn send_to_client_async(
1150        &self,
1151        client_id: Uuid,
1152        data: Arc<Bytes>,
1153    ) -> Result<(), SendError> {
1154        // Check if client token has expired before sending
1155        if self.check_and_remove_expired(client_id) {
1156            return Err(SendError::ClientDisconnected);
1157        }
1158
1159        // Check per-connection and aggregate account egress limits
1160        self.enforce_egress_budgets(client_id, data.len())?;
1161
1162        let sender = {
1163            let client = self
1164                .clients
1165                .get(&client_id)
1166                .ok_or(SendError::ClientNotFound)?;
1167            client.sender.clone()
1168        };
1169
1170        let msg = Message::Binary((*data).clone());
1171        sender
1172            .send(msg)
1173            .await
1174            .map_err(|_| SendError::ClientDisconnected)
1175    }
1176
1177    /// Send a text message to a specific client (async).
1178    ///
1179    /// This method sends a text message directly to the client's WebSocket.
1180    /// Used for control messages like auth refresh responses.
1181    pub async fn send_text_to_client(
1182        &self,
1183        client_id: Uuid,
1184        text: String,
1185    ) -> Result<(), SendError> {
1186        // Check if client token has expired before sending
1187        if self.check_and_remove_expired(client_id) {
1188            return Err(SendError::ClientDisconnected);
1189        }
1190
1191        let sender = {
1192            let client = self
1193                .clients
1194                .get(&client_id)
1195                .ok_or(SendError::ClientNotFound)?;
1196            client.sender.clone()
1197        };
1198
1199        let msg = Message::Text(text.into());
1200        sender
1201            .send(msg)
1202            .await
1203            .map_err(|_| SendError::ClientDisconnected)
1204    }
1205
1206    /// Send a potentially compressed payload to a client (async).
1207    ///
1208    /// Compressed payloads are sent as binary frames (raw gzip).
1209    /// Uncompressed payloads are sent as text frames (JSON).
1210    pub async fn send_compressed_async(
1211        &self,
1212        client_id: Uuid,
1213        payload: CompressedPayload,
1214    ) -> Result<(), SendError> {
1215        // Check if client token has expired before sending
1216        if self.check_and_remove_expired(client_id) {
1217            return Err(SendError::ClientDisconnected);
1218        }
1219
1220        let (sender, bytes_to_record) = {
1221            let client = self
1222                .clients
1223                .get(&client_id)
1224                .ok_or(SendError::ClientNotFound)?;
1225
1226            let bytes = match &payload {
1227                CompressedPayload::Compressed(bytes) => bytes.len(),
1228                CompressedPayload::Uncompressed(bytes) => bytes.len(),
1229            };
1230
1231            (client.sender.clone(), bytes)
1232        };
1233
1234        // Check per-connection and aggregate account egress limits
1235        self.enforce_egress_budgets(client_id, bytes_to_record)?;
1236
1237        let msg = match payload {
1238            CompressedPayload::Compressed(bytes) => Message::Binary(bytes),
1239            CompressedPayload::Uncompressed(bytes) => Message::Binary(bytes),
1240        };
1241        sender
1242            .send(msg)
1243            .await
1244            .map_err(|_| SendError::ClientDisconnected)
1245    }
1246
1247    /// Update the last_seen timestamp for a client.
1248    pub fn update_client_last_seen(&self, client_id: Uuid) {
1249        if let Some(mut client) = self.clients.get_mut(&client_id) {
1250            client.update_last_seen();
1251        }
1252    }
1253
1254    /// Check whether an inbound message is allowed for a client.
1255    #[allow(clippy::result_large_err)]
1256    pub fn check_inbound_message_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
1257        if self.check_and_remove_expired(client_id) {
1258            return Err(AuthDeny::new(
1259                crate::websocket::auth::AuthErrorCode::TokenExpired,
1260                "Authentication token expired",
1261            ));
1262        }
1263
1264        let (connection_ok, account_ok, permit_id) = {
1265            let Some(client) = self.clients.get(&client_id) else {
1266                return Err(AuthDeny::new(
1267                    crate::websocket::auth::AuthErrorCode::InternalError,
1268                    "Client not found",
1269                ));
1270            };
1271            let connection_ok = client.record_inbound_message().is_some();
1272            let account_ok = !connection_ok
1273                || client
1274                    .auth_context
1275                    .as_ref()
1276                    .map(|ctx| self.record_account_message(ctx))
1277                    .unwrap_or(true);
1278            let permit_id = client.admission_permit.as_ref().map(|permit| permit.id());
1279            (connection_ok, account_ok, permit_id)
1280        };
1281
1282        if connection_ok && account_ok {
1283            if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1284                provider.check_inbound_message(permit_id)?;
1285            }
1286            return Ok(());
1287        }
1288        let scope = if connection_ok {
1289            "inbound account websocket messages"
1290        } else {
1291            "inbound websocket messages"
1292        };
1293        Err(
1294            AuthDeny::rate_limited(self.rate_limit_config.message_rate_window, scope).with_context(
1295                format!("client {} exceeded the inbound message budget", client_id),
1296            ),
1297        )
1298    }
1299
1300    /// Check if a client exists.
1301    pub fn has_client(&self, client_id: Uuid) -> bool {
1302        self.clients.contains_key(&client_id)
1303    }
1304
1305    /// A token cancelled when `client_id` leaves the registry, for any reason.
1306    ///
1307    /// Already cancelled when the client is not registered. Subscription
1308    /// tokens should be [`CancellationToken::child_token`]s of it, so an
1309    /// eviction stops their delivery tasks as surely as a disconnect does.
1310    pub fn connection_token(&self, client_id: Uuid) -> CancellationToken {
1311        match self.clients.get(&client_id) {
1312            Some(client) => client.connection_token.clone(),
1313            None => {
1314                let token = CancellationToken::new();
1315                token.cancel();
1316                token
1317            }
1318        }
1319    }
1320
1321    pub async fn add_client_subscription(
1322        &self,
1323        client_id: Uuid,
1324        subscription_id: String,
1325        token: CancellationToken,
1326    ) -> bool {
1327        self.try_add_client_subscription(client_id, subscription_id, token)
1328            .await
1329            .unwrap_or(false)
1330    }
1331
1332    #[allow(clippy::result_large_err)]
1333    pub(crate) async fn try_add_client_subscription(
1334        &self,
1335        client_id: Uuid,
1336        subscription_id: String,
1337        token: CancellationToken,
1338    ) -> Result<bool, AuthDeny> {
1339        let Some(lifecycle) = self
1340            .clients
1341            .get(&client_id)
1342            .map(|client| client.admission_lifecycle.clone())
1343        else {
1344            return Ok(false);
1345        };
1346        let _lifecycle = lifecycle.lock().await;
1347        let Some((subscriptions, permit)) = self.clients.get(&client_id).and_then(|client| {
1348            Arc::ptr_eq(&client.admission_lifecycle, &lifecycle).then(|| {
1349                (
1350                    client.subscriptions.clone(),
1351                    client.admission_permit.clone(),
1352                )
1353            })
1354        }) else {
1355            return Ok(false);
1356        };
1357        let permit_id = permit.as_ref().map(|permit| permit.id());
1358        if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1359            if !provider.add_subscription(permit_id, &subscription_id)? {
1360                return Ok(false);
1361            }
1362        }
1363        let mut subscriptions = subscriptions.write().await;
1364        let client = self.clients.get(&client_id);
1365        let registered = client
1366            .as_ref()
1367            .is_some_and(|client| Arc::ptr_eq(&client.admission_lifecycle, &lifecycle));
1368        if !registered || subscriptions.contains_key(&subscription_id) {
1369            drop(client);
1370            drop(subscriptions);
1371            if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1372                provider.remove_subscription(permit_id, &subscription_id);
1373            }
1374            return Ok(false);
1375        }
1376        subscriptions.insert(subscription_id, token);
1377        drop(client);
1378        Ok(true)
1379    }
1380
1381    pub async fn remove_client_subscription(&self, client_id: Uuid, subscription_id: &str) -> bool {
1382        let Some(lifecycle) = self
1383            .clients
1384            .get(&client_id)
1385            .map(|client| client.admission_lifecycle.clone())
1386        else {
1387            return false;
1388        };
1389        let _lifecycle = lifecycle.lock().await;
1390        let Some((subscriptions, permit)) = self.clients.get(&client_id).and_then(|client| {
1391            Arc::ptr_eq(&client.admission_lifecycle, &lifecycle).then(|| {
1392                (
1393                    client.subscriptions.clone(),
1394                    client.admission_permit.clone(),
1395                )
1396            })
1397        }) else {
1398            return false;
1399        };
1400        let permit_id = permit.as_ref().map(|permit| permit.id());
1401        let mut subscriptions = subscriptions.write().await;
1402        let client = self.clients.get(&client_id);
1403        if !client
1404            .as_ref()
1405            .is_some_and(|client| Arc::ptr_eq(&client.admission_lifecycle, &lifecycle))
1406        {
1407            return false;
1408        }
1409        let token = subscriptions.remove(subscription_id);
1410        drop(client);
1411        drop(subscriptions);
1412        if let Some(token) = token {
1413            token.cancel();
1414            if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1415                provider.remove_subscription(permit_id, subscription_id);
1416            }
1417            true
1418        } else {
1419            false
1420        }
1421    }
1422
1423    /// Give an active subscription a new cancellation token, cancelling the
1424    /// one it had, without releasing its registration or admission: its
1425    /// delivery stops and the caller may attach a new one under the same ID.
1426    /// Returns `false` when the client or subscription is gone.
1427    pub async fn replace_client_subscription_token(
1428        &self,
1429        client_id: Uuid,
1430        subscription_id: &str,
1431        cancel_token: CancellationToken,
1432    ) -> bool {
1433        let Some(lifecycle) = self
1434            .clients
1435            .get(&client_id)
1436            .map(|client| client.admission_lifecycle.clone())
1437        else {
1438            return false;
1439        };
1440        let _lifecycle = lifecycle.lock().await;
1441        let Some(subscriptions) = self.clients.get(&client_id).and_then(|client| {
1442            Arc::ptr_eq(&client.admission_lifecycle, &lifecycle)
1443                .then(|| client.subscriptions.clone())
1444        }) else {
1445            return false;
1446        };
1447        let mut subscriptions = subscriptions.write().await;
1448        match subscriptions.get_mut(subscription_id) {
1449            Some(token) => {
1450                std::mem::replace(token, cancel_token).cancel();
1451                true
1452            }
1453            None => false,
1454        }
1455    }
1456
1457    pub async fn cancel_all_client_subscriptions(&self, client_id: Uuid) {
1458        let Some(lifecycle) = self
1459            .clients
1460            .get(&client_id)
1461            .map(|client| client.admission_lifecycle.clone())
1462        else {
1463            return;
1464        };
1465        let _lifecycle = lifecycle.lock().await;
1466        let Some(subscriptions) = self.clients.get(&client_id).and_then(|client| {
1467            Arc::ptr_eq(&client.admission_lifecycle, &lifecycle)
1468                .then(|| client.subscriptions.clone())
1469        }) else {
1470            return;
1471        };
1472        let subscriptions = subscriptions.read().await;
1473        for (subscription_id, token) in subscriptions.iter() {
1474            token.cancel();
1475            debug!("Cancelled subscription on disconnect: {}", subscription_id);
1476        }
1477    }
1478
1479    /// Remove stale clients that haven't been seen within the timeout period.
1480    pub fn cleanup_stale_clients(&self) -> usize {
1481        let timeout = self.rate_limit_config.client_timeout;
1482        let mut stale_clients = Vec::new();
1483
1484        for entry in self.clients.iter() {
1485            if entry.value().is_stale(timeout) {
1486                stale_clients.push(*entry.key());
1487            }
1488        }
1489
1490        let removed_count = stale_clients.len();
1491        for client_id in stale_clients {
1492            self.clients.remove(&client_id);
1493            info!("Removed stale client {}", client_id);
1494        }
1495
1496        removed_count
1497    }
1498
1499    /// Evict aggregate account usage and policy state for accounts with no
1500    /// live connection past the idle TTL.
1501    fn cleanup_account_state(&self) {
1502        let live_accounts: HashSet<String> = self
1503            .clients
1504            .iter()
1505            .filter_map(|entry| {
1506                entry
1507                    .value()
1508                    .auth_context
1509                    .as_ref()
1510                    .and_then(|ctx| ctx.account_key.clone())
1511            })
1512            .collect();
1513        self.account_usage.retain(|account, usage| {
1514            live_accounts.contains(account) || usage.idle_for() < ACCOUNT_USAGE_IDLE_TTL
1515        });
1516        self.account_policies
1517            .evict_idle(|account| live_accounts.contains(account));
1518    }
1519
1520    /// Start a background task that periodically cleans up stale clients.
1521    ///
1522    /// Returns the task so an owner that stops serving can abort it; callers
1523    /// that run for the life of the process may simply drop the handle.
1524    pub fn start_cleanup_task(&self) -> tokio::task::JoinHandle<()> {
1525        let client_manager = self.clone();
1526
1527        tokio::spawn(async move {
1528            let mut interval = tokio::time::interval(Duration::from_secs(30));
1529
1530            loop {
1531                interval.tick().await;
1532                let removed = client_manager.cleanup_stale_clients();
1533                if removed > 0 {
1534                    info!("Cleaned up {} stale clients", removed);
1535                }
1536                client_manager.cleanup_account_state();
1537                if let Some(provider) = &client_manager.admission_provider {
1538                    provider.cleanup();
1539                }
1540            }
1541        })
1542    }
1543
1544    /// ENFORCEMENT HOOKS
1545    ///
1546    /// These methods provide hooks for enforcing limits based on auth context.
1547    /// They check limits before allowing operations and return errors if limits are exceeded.
1548    /// Check if a connection is allowed for the given auth context.
1549    ///
1550    /// Returns Ok(()) if the connection is allowed, or an error with a reason if not.
1551    ///
1552    /// Publishable-key sessions get one fixed reason per refusal: the detailed
1553    /// reasons name accounts, consumers and connection counts.
1554    #[allow(clippy::result_large_err)]
1555    pub async fn check_connection_allowed(
1556        &self,
1557        remote_addr: SocketAddr,
1558        auth_context: &Option<AuthContext>,
1559    ) -> Result<(), AuthDeny> {
1560        let publishable = auth_context
1561            .as_ref()
1562            .is_some_and(|ctx| ctx.key_class == arete_auth::KeyClass::Publishable);
1563        self.check_connection_limits(remote_addr, auth_context)
1564            .await
1565            .map_err(|deny| {
1566                if publishable {
1567                    deny.for_publishable_session()
1568                } else {
1569                    deny
1570                }
1571            })
1572    }
1573
1574    #[allow(clippy::result_large_err)]
1575    async fn check_connection_limits(
1576        &self,
1577        remote_addr: SocketAddr,
1578        auth_context: &Option<AuthContext>,
1579    ) -> Result<(), AuthDeny> {
1580        // Check rate limiter first if configured
1581        if let Some(ref rate_limiter) = self.rate_limiter {
1582            // Check handshake rate limit for IP
1583            match rate_limiter.check_handshake(remote_addr).await {
1584                RateLimitResult::Allowed { .. } => {}
1585                RateLimitResult::Denied { retry_after, limit } => {
1586                    return Err(AuthDeny::rate_limited(retry_after, "websocket handshakes")
1587                        .with_context(format!(
1588                            "handshake rate limit of {} per minute exceeded for {}",
1589                            limit, remote_addr
1590                        )));
1591                }
1592            }
1593
1594            if let Some(ref ctx) = auth_context {
1595                // Connection-attempt rate per resolved consumer. Legacy
1596                // tokens resolve to the subject with the configured window,
1597                // preserving old behavior exactly.
1598                match rate_limiter
1599                    .check_connection_for_consumer(
1600                        ctx.consumer_key(),
1601                        ctx.limits.max_connection_attempts_per_minute,
1602                    )
1603                    .await
1604                {
1605                    RateLimitResult::Allowed { .. } => {}
1606                    RateLimitResult::Denied { retry_after, limit } => {
1607                        return Err(AuthDeny::rate_limited(retry_after, "websocket connections")
1608                            .with_context(format!(
1609                                "connection rate limit for consumer {} of {} per minute exceeded",
1610                                ctx.consumer_key(),
1611                                limit
1612                            )));
1613                    }
1614                }
1615
1616                // Connection-attempt rate per resolved account. Legacy
1617                // tokens resolve to the metering key; anonymous v2 tokens
1618                // carry no account and skip the aggregate bucket.
1619                if ctx.is_legacy_policy() || ctx.account_key.is_some() {
1620                    match rate_limiter
1621                        .check_connection_for_account(
1622                            ctx.account_key(),
1623                            ctx.account_limits.max_connection_attempts_per_minute,
1624                        )
1625                        .await
1626                    {
1627                        RateLimitResult::Allowed { .. } => {}
1628                        RateLimitResult::Denied { retry_after, limit } => {
1629                            return Err(AuthDeny::rate_limited(
1630                                retry_after,
1631                                "metered websocket connections",
1632                            )
1633                            .with_context(format!(
1634                                "connection rate limit for account {} of {} per minute exceeded",
1635                                ctx.account_key(),
1636                                limit
1637                            )));
1638                        }
1639                    }
1640                }
1641            }
1642        }
1643
1644        // Check global per-IP connection limit
1645        if let Some(max_per_ip) = self.rate_limit_config.max_connections_per_ip {
1646            let current_ip_connections = self.count_connections_for_ip(&remote_addr);
1647            if current_ip_connections >= max_per_ip {
1648                return Err(AuthDeny::connection_limit_exceeded(
1649                    &format!("ip {}", remote_addr.ip()),
1650                    current_ip_connections,
1651                    max_per_ip,
1652                ));
1653            }
1654        }
1655
1656        if let Some(ctx) = auth_context {
1657            self.check_identity_limits(ctx, None)?;
1658        }
1659        Ok(())
1660    }
1661
1662    /// The admission checks tied to a session's identity rather than its
1663    /// socket: account policy version and the concurrent-connection caps per
1664    /// consumer, account, metering key and origin.
1665    ///
1666    /// The handshake runs them before the client is registered. An in-band
1667    /// refresh runs them for the incoming token with `refreshing` set, so the
1668    /// socket being refreshed is not counted against itself: it may keep its
1669    /// place under its current identity, but cannot move into one already at
1670    /// its cap, or onto a policy version older than one already observed.
1671    #[allow(clippy::result_large_err)]
1672    fn check_identity_limits(
1673        &self,
1674        ctx: &AuthContext,
1675        refreshing: Option<Uuid>,
1676    ) -> Result<(), AuthDeny> {
1677        // Admit the token against previously observed account policy and
1678        // count legacy tokens so legacy support can be retired once none remain.
1679        if ctx.is_legacy_policy() {
1680            let legacy_policy_token = self.account_policies.record_legacy_token();
1681            debug!(legacy_policy_token, "legacy policy token admitted");
1682        } else if let (Some(account), Some(policy_version)) =
1683            (ctx.account_key.as_deref(), ctx.policy_version)
1684        {
1685            match self
1686                .account_policies
1687                .observe(account, policy_version, &ctx.account_limits)
1688            {
1689                Ok(()) => {}
1690                Err(AccountPolicyError::StaleVersion { presented, current }) => {
1691                    debug!(
1692                        account = %redact_identity(account),
1693                        presented,
1694                        current,
1695                        "stale policy version rejected"
1696                    );
1697                    return Err(AuthDeny::new(
1698                        crate::websocket::auth::AuthErrorCode::TokenExpired,
1699                        "session policy version is stale; refresh the session token",
1700                    ));
1701                }
1702                Err(AccountPolicyError::ConflictingLimits { version }) => {
1703                    warn!(
1704                        account = %redact_identity(account),
1705                        version,
1706                        "signed account limits conflict for one policy version"
1707                    );
1708                    return Err(AuthDeny::new(
1709                        crate::websocket::auth::AuthErrorCode::InternalError,
1710                        "signed account limits conflict with previously observed policy",
1711                    ));
1712                }
1713                Err(AccountPolicyError::CapacityExhausted) => {
1714                    warn!("account policy state at capacity; denying admission");
1715                    return Err(AuthDeny::new(
1716                        crate::websocket::auth::AuthErrorCode::InternalError,
1717                        "account policy state is at capacity; retry shortly",
1718                    ));
1719                }
1720            }
1721        }
1722
1723        // Check max connections per resolved consumer (token limits,
1724        // fallback to default limits). Legacy tokens resolve to the
1725        // subject, preserving old behavior.
1726        let max_connections = ctx.limits.max_connections.or_else(|| {
1727            self.rate_limit_config
1728                .default_limits
1729                .as_ref()
1730                .and_then(|l| l.max_connections)
1731        });
1732        if let Some(max_connections) = max_connections {
1733            let current_connections =
1734                self.count_connections_for_consumer(ctx.consumer_key(), refreshing);
1735            if current_connections >= max_connections as usize {
1736                return Err(AuthDeny::connection_limit_exceeded(
1737                    &format!("consumer {}", ctx.consumer_key()),
1738                    current_connections,
1739                    max_connections as usize,
1740                ));
1741            }
1742        }
1743
1744        // Check aggregate concurrent connections per account from the
1745        // signed account limits.
1746        if !ctx.is_legacy_policy() && ctx.account_key.is_some() {
1747            if let Some(max_account_connections) = ctx.account_limits.max_connections {
1748                let current_account_connections =
1749                    self.count_connections_for_account(ctx.account_key(), refreshing);
1750                if current_account_connections >= max_account_connections as usize {
1751                    return Err(AuthDeny::connection_limit_exceeded(
1752                        &format!("account {}", ctx.account_key()),
1753                        current_account_connections,
1754                        max_account_connections as usize,
1755                    ));
1756                }
1757            }
1758        }
1759
1760        // Check global max connections per metering key
1761        if let Some(max_per_metering_key) = self.rate_limit_config.max_connections_per_metering_key
1762        {
1763            let current_metering_connections =
1764                self.count_connections_for_metering_key(&ctx.metering_key, refreshing);
1765            if current_metering_connections >= max_per_metering_key {
1766                return Err(AuthDeny::connection_limit_exceeded(
1767                    &format!("metering key {}", ctx.metering_key),
1768                    current_metering_connections,
1769                    max_per_metering_key,
1770                ));
1771            }
1772        }
1773
1774        // Check global max connections per origin
1775        if let Some(max_per_origin) = self.rate_limit_config.max_connections_per_origin {
1776            if let Some(ref origin) = ctx.origin {
1777                let current_origin_connections =
1778                    self.count_connections_for_origin(origin, refreshing);
1779                if current_origin_connections >= max_per_origin {
1780                    return Err(AuthDeny::connection_limit_exceeded(
1781                        &format!("origin {}", origin),
1782                        current_origin_connections,
1783                        max_per_origin,
1784                    ));
1785                }
1786            }
1787        }
1788        Ok(())
1789    }
1790
1791    /// Count connections from a specific IP address
1792    fn count_connections_for_ip(&self, remote_addr: &SocketAddr) -> usize {
1793        let ip = remote_addr.ip();
1794        self.clients
1795            .iter()
1796            .filter(|entry| entry.value().remote_addr.ip() == ip)
1797            .count()
1798    }
1799
1800    /// Count connections for a resolved consumer identity
1801    fn count_connections_for_consumer(&self, consumer: &str, exclude: Option<Uuid>) -> usize {
1802        self.clients
1803            .iter()
1804            .filter(|entry| Some(*entry.key()) != exclude)
1805            .filter(|entry| {
1806                entry
1807                    .value()
1808                    .auth_context
1809                    .as_ref()
1810                    .map(|ctx| ctx.consumer_key() == consumer)
1811                    .unwrap_or(false)
1812            })
1813            .count()
1814    }
1815
1816    /// Count connections for a resolved account identity
1817    fn count_connections_for_account(&self, account: &str, exclude: Option<Uuid>) -> usize {
1818        self.clients
1819            .iter()
1820            .filter(|entry| Some(*entry.key()) != exclude)
1821            .filter(|entry| {
1822                entry
1823                    .value()
1824                    .auth_context
1825                    .as_ref()
1826                    .map(|ctx| ctx.account_key() == account)
1827                    .unwrap_or(false)
1828            })
1829            .count()
1830    }
1831
1832    /// Count connections for a specific metering key
1833    fn count_connections_for_metering_key(
1834        &self,
1835        metering_key: &str,
1836        exclude: Option<Uuid>,
1837    ) -> usize {
1838        self.clients
1839            .iter()
1840            .filter(|entry| Some(*entry.key()) != exclude)
1841            .filter(|entry| {
1842                entry
1843                    .value()
1844                    .auth_context
1845                    .as_ref()
1846                    .map(|ctx| ctx.metering_key == metering_key)
1847                    .unwrap_or(false)
1848            })
1849            .count()
1850    }
1851
1852    /// Count connections for a specific origin
1853    fn count_connections_for_origin(&self, origin: &str, exclude: Option<Uuid>) -> usize {
1854        self.clients
1855            .iter()
1856            .filter(|entry| Some(*entry.key()) != exclude)
1857            .filter(|entry| {
1858                entry
1859                    .value()
1860                    .auth_context
1861                    .as_ref()
1862                    .and_then(|ctx| ctx.origin.as_ref())
1863                    .map(|o| o == origin)
1864                    .unwrap_or(false)
1865            })
1866            .count()
1867    }
1868
1869    /// Check if a subscription is allowed for the given client.
1870    ///
1871    /// Returns Ok(()) if the subscription is allowed, or an error with a reason if not.
1872    #[allow(clippy::result_large_err)]
1873    pub async fn check_subscription_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
1874        let context = {
1875            let Some(client) = self.clients.get(&client_id) else {
1876                return Ok(());
1877            };
1878            let current_subs = client.subscription_count().await;
1879
1880            // Check max subscriptions per connection (use token limits, fallback to default limits)
1881            if let Some(ref ctx) = client.auth_context {
1882                let max_subs = ctx.limits.max_subscriptions.or_else(|| {
1883                    self.rate_limit_config
1884                        .default_limits
1885                        .as_ref()
1886                        .and_then(|l| l.max_subscriptions)
1887                });
1888                if let Some(max_subs) = max_subs {
1889                    if current_subs >= max_subs as usize {
1890                        return Err(AuthDeny::new(
1891                            crate::websocket::auth::AuthErrorCode::SubscriptionLimitExceeded,
1892                            format!(
1893                                "Subscription limit exceeded: {} of {} subscriptions for client {}",
1894                                current_subs, max_subs, client_id
1895                            ),
1896                        )
1897                        .with_suggested_action(
1898                            "Unsubscribe from an existing view before creating another subscription",
1899                        ));
1900                    }
1901                }
1902            }
1903            client.auth_context.clone()
1904        };
1905
1906        // Signed subscription-create rates, enforced only for v2 tokens that
1907        // carry the corresponding optional limit fields.
1908        if let (Some(rate_limiter), Some(ctx)) = (self.rate_limiter.as_ref(), context.as_ref()) {
1909            if !ctx.is_legacy_policy() {
1910                if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1911                    .check_subscription_create_for_consumer(
1912                        ctx.consumer_key(),
1913                        ctx.limits.max_subscription_creates_per_minute,
1914                    )
1915                    .await
1916                {
1917                    return Err(AuthDeny::rate_limited(retry_after, "subscription creates")
1918                        .with_context(format!(
1919                        "subscription-create rate limit for consumer {} of {} per minute exceeded",
1920                        ctx.consumer_key(),
1921                        limit
1922                    )));
1923                }
1924
1925                if ctx.account_key.is_some() {
1926                    if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1927                        .check_subscription_create_for_account(
1928                            ctx.account_key(),
1929                            ctx.account_limits.max_subscription_creates_per_minute,
1930                        )
1931                        .await
1932                    {
1933                        return Err(AuthDeny::rate_limited(
1934                            retry_after,
1935                            "account subscription creates",
1936                        )
1937                        .with_context(format!(
1938                            "subscription-create rate limit for account {} of {} per minute exceeded",
1939                            ctx.account_key(),
1940                            limit
1941                        )));
1942                    }
1943                }
1944            }
1945        }
1946        Ok(())
1947    }
1948
1949    /// Get metering key for a client
1950    pub fn get_metering_key(&self, client_id: Uuid) -> Option<String> {
1951        self.clients.get(&client_id).and_then(|client| {
1952            client
1953                .auth_context
1954                .as_ref()
1955                .map(|ctx| ctx.metering_key.clone())
1956        })
1957    }
1958
1959    /// Get auth context for a client.
1960    pub fn get_auth_context(&self, client_id: Uuid) -> Option<AuthContext> {
1961        self.clients
1962            .get(&client_id)
1963            .and_then(|client| client.auth_context.clone())
1964    }
1965
1966    /// Check if a snapshot request is allowed (based on max_snapshot_rows limit)
1967    ///
1968    /// Uses token limits if available, falls back to default limits from RateLimitConfig.
1969    #[allow(clippy::result_large_err)]
1970    pub fn check_snapshot_allowed(
1971        &self,
1972        client_id: Uuid,
1973        requested_rows: u32,
1974    ) -> Result<(), AuthDeny> {
1975        if let Some(client) = self.clients.get(&client_id) {
1976            if let Some(ref ctx) = client.auth_context {
1977                let max_rows = ctx.limits.max_snapshot_rows.or_else(|| {
1978                    self.rate_limit_config
1979                        .default_limits
1980                        .as_ref()
1981                        .and_then(|l| l.max_snapshot_rows)
1982                });
1983                if let Some(max_rows) = max_rows {
1984                    if requested_rows > max_rows {
1985                        return Err(AuthDeny::new(
1986                            crate::websocket::auth::AuthErrorCode::SnapshotLimitExceeded,
1987                            format!(
1988                                "Snapshot limit exceeded: requested {} rows, max allowed is {} for client {}",
1989                                requested_rows, max_rows, client_id
1990                            ),
1991                        )
1992                        .with_suggested_action(
1993                            "Request fewer rows or lower the snapshotLimit on the subscription",
1994                        ));
1995                    }
1996                }
1997            }
1998        }
1999        Ok(())
2000    }
2001}
2002
2003impl Default for ClientManager {
2004    fn default() -> Self {
2005        Self::new()
2006    }
2007}
2008
2009#[cfg(test)]
2010#[path = "admission_tests.rs"]
2011mod admission_tests;
2012
2013#[cfg(test)]
2014mod tests {
2015    use super::*;
2016    use crate::websocket::auth::AuthContext;
2017    use arete_auth::{KeyClass, Limits};
2018    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
2019
2020    fn create_test_auth_context(subject: &str, limits: Limits) -> AuthContext {
2021        AuthContext {
2022            subject: subject.to_string(),
2023            issuer: "test-issuer".to_string(),
2024            audience: "test-audience".to_string(),
2025            key_class: KeyClass::Publishable,
2026            metering_key: format!("meter-{}", subject),
2027            deployment_id: None,
2028            target_kind: None,
2029            target_id: None,
2030            program_id: None,
2031            program_release_hash: None,
2032            expires_at: u64::MAX,
2033            scope: "read".to_string(),
2034            limits,
2035            plan: None,
2036            origin: None,
2037            client_ip: None,
2038            jti: uuid::Uuid::new_v4().to_string(),
2039            actor_key: None,
2040            account_key: None,
2041            consumer_key: None,
2042            policy_version: None,
2043            account_limits: Limits::default(),
2044            row_scopes: None,
2045        }
2046    }
2047
2048    fn create_v2_auth_context(
2049        consumer: &str,
2050        account: &str,
2051        policy_version: u32,
2052        limits: Limits,
2053        account_limits: Limits,
2054    ) -> AuthContext {
2055        AuthContext {
2056            subject: "user:1".to_string(),
2057            issuer: "test-issuer".to_string(),
2058            audience: "test-audience".to_string(),
2059            key_class: KeyClass::Publishable,
2060            metering_key: account.to_string(),
2061            deployment_id: None,
2062            target_kind: None,
2063            target_id: None,
2064            program_id: None,
2065            program_release_hash: None,
2066            expires_at: u64::MAX,
2067            scope: "read".to_string(),
2068            limits,
2069            plan: Some("pro".to_string()),
2070            origin: None,
2071            client_ip: None,
2072            jti: uuid::Uuid::new_v4().to_string(),
2073            actor_key: Some("user:1".to_string()),
2074            account_key: Some(account.to_string()),
2075            consumer_key: Some(consumer.to_string()),
2076            policy_version: Some(policy_version),
2077            account_limits,
2078            row_scopes: None,
2079        }
2080    }
2081
2082    fn insert_client(manager: &ClientManager, context: AuthContext) -> Uuid {
2083        let (sender, receiver) = mpsc::channel(8);
2084        // Keep the receiver alive so the sender stays open.
2085        std::mem::forget(receiver);
2086        let client_id = Uuid::new_v4();
2087        manager.clients.insert(
2088            client_id,
2089            ClientInfo::new(
2090                client_id,
2091                sender,
2092                Some(context),
2093                create_test_socket_addr("127.0.0.1"),
2094            ),
2095        );
2096        client_id
2097    }
2098
2099    fn create_test_socket_addr(ip: &str) -> SocketAddr {
2100        SocketAddr::new(
2101            ip.parse::<IpAddr>()
2102                .unwrap_or(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
2103            12345,
2104        )
2105    }
2106
2107    #[test]
2108    fn test_egress_tracker_basic() {
2109        let mut tracker = EgressTracker::new();
2110
2111        // Should allow bytes within limit
2112        assert!(tracker.record_bytes(500, 1000));
2113        assert_eq!(tracker.current_usage(), 500);
2114
2115        // Should allow more bytes within limit
2116        assert!(tracker.record_bytes(400, 1000));
2117        assert_eq!(tracker.current_usage(), 900);
2118
2119        // Should reject bytes over limit
2120        assert!(!tracker.record_bytes(200, 1000));
2121        assert_eq!(tracker.current_usage(), 900); // Usage shouldn't increase
2122    }
2123
2124    #[test]
2125    fn test_egress_tracker_window_reset() {
2126        let mut tracker = EgressTracker::new();
2127
2128        // Use up the limit
2129        assert!(tracker.record_bytes(100, 100));
2130        assert!(!tracker.record_bytes(1, 100));
2131
2132        // Reset the window
2133        tracker.bytes_this_minute = 0;
2134        tracker.window_start = SystemTime::now() - Duration::from_secs(61);
2135
2136        // Should allow after window reset
2137        assert!(tracker.record_bytes(50, 100));
2138    }
2139
2140    #[test]
2141    fn test_message_rate_tracker_basic() {
2142        let mut tracker = MessageRateTracker::new();
2143
2144        assert!(tracker.record_message(2));
2145        assert_eq!(tracker.current_usage(), 1);
2146
2147        assert!(tracker.record_message(2));
2148        assert_eq!(tracker.current_usage(), 2);
2149
2150        assert!(!tracker.record_message(2));
2151        assert_eq!(tracker.current_usage(), 2);
2152    }
2153
2154    #[tokio::test]
2155    async fn test_client_inbound_message_limit() {
2156        let (tx, _rx) = mpsc::channel(1);
2157        let client = ClientInfo::new(
2158            Uuid::new_v4(),
2159            tx,
2160            Some(create_test_auth_context(
2161                "user-1",
2162                Limits {
2163                    max_messages_per_minute: Some(2),
2164                    ..Default::default()
2165                },
2166            )),
2167            create_test_socket_addr("127.0.0.1"),
2168        );
2169
2170        assert_eq!(client.record_inbound_message(), Some(1));
2171        assert_eq!(client.record_inbound_message(), Some(2));
2172        assert_eq!(client.record_inbound_message(), None);
2173    }
2174
2175    #[tokio::test]
2176    async fn duplicate_subscription_id_is_rejected_without_replacement() {
2177        let (tx, _rx) = mpsc::channel(1);
2178        let client = ClientInfo::new(
2179            Uuid::new_v4(),
2180            tx,
2181            None,
2182            create_test_socket_addr("127.0.0.1"),
2183        );
2184        let first = CancellationToken::new();
2185        let duplicate = CancellationToken::new();
2186
2187        assert!(
2188            client
2189                .add_subscription("opaque-id".to_string(), first.clone())
2190                .await
2191        );
2192        assert!(
2193            !client
2194                .add_subscription("opaque-id".to_string(), duplicate.clone())
2195                .await
2196        );
2197        assert!(!first.is_cancelled());
2198        assert!(!duplicate.is_cancelled());
2199
2200        assert!(client.remove_subscription("opaque-id").await);
2201        assert!(first.is_cancelled());
2202        assert!(!duplicate.is_cancelled());
2203    }
2204
2205    #[tokio::test]
2206    async fn test_no_limits() {
2207        let manager = ClientManager::new();
2208        let addr = create_test_socket_addr("127.0.0.1");
2209
2210        // No auth context - should succeed
2211        assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
2212
2213        // Auth context with no limits - should succeed
2214        let auth_context = create_test_auth_context("test", Limits::default());
2215        assert!(manager
2216            .check_connection_allowed(addr, &Some(auth_context))
2217            .await
2218            .is_ok());
2219    }
2220
2221    #[tokio::test]
2222    async fn test_per_subject_connection_limit() {
2223        let manager = ClientManager::new();
2224
2225        let limits = Limits {
2226            max_connections: Some(2),
2227            ..Default::default()
2228        };
2229
2230        let auth_context = create_test_auth_context("user-1", limits);
2231        let addr = create_test_socket_addr("127.0.0.1");
2232
2233        // First connection should succeed (no clients yet)
2234        assert!(manager
2235            .check_connection_allowed(addr, &Some(auth_context.clone()))
2236            .await
2237            .is_ok());
2238    }
2239
2240    #[tokio::test]
2241    async fn test_per_ip_connection_limit() {
2242        let manager = ClientManager::new().with_max_connections_per_ip(2);
2243        let addr = create_test_socket_addr("192.168.1.1");
2244
2245        // Should succeed when no connections from that IP
2246        assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
2247    }
2248
2249    // Tests for RateLimitConfig
2250    #[test]
2251    fn rate_limit_config_default() {
2252        let config = RateLimitConfig::default();
2253        assert!(config.max_connections_per_ip.is_none());
2254        assert_eq!(config.client_timeout, Duration::from_secs(300));
2255        assert_eq!(config.message_queue_size, 512);
2256        assert!(config.max_reconnect_attempts.is_none());
2257        assert_eq!(config.message_rate_window, Duration::from_secs(60));
2258        assert_eq!(config.egress_rate_window, Duration::from_secs(60));
2259    }
2260
2261    #[test]
2262    fn rate_limit_config_builder_methods() {
2263        let config = RateLimitConfig::default()
2264            .with_max_connections_per_ip(10)
2265            .with_timeout(Duration::from_secs(600))
2266            .with_message_queue_size(1024)
2267            .with_rate_limit_window(Duration::from_secs(120));
2268
2269        assert_eq!(config.max_connections_per_ip, Some(10));
2270        assert_eq!(config.client_timeout, Duration::from_secs(600));
2271        assert_eq!(config.message_queue_size, 1024);
2272        assert_eq!(config.message_rate_window, Duration::from_secs(120));
2273        assert_eq!(config.egress_rate_window, Duration::from_secs(120));
2274    }
2275
2276    #[tokio::test]
2277    async fn client_manager_with_config() {
2278        let config = RateLimitConfig::default()
2279            .with_max_connections_per_ip(5)
2280            .with_timeout(Duration::from_secs(120))
2281            .with_message_queue_size(256);
2282
2283        let manager = ClientManager::with_config(config);
2284        let addr = create_test_socket_addr("10.0.0.1");
2285
2286        // Check that the configuration was applied
2287        assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(5));
2288        assert_eq!(
2289            manager.rate_limit_config().client_timeout,
2290            Duration::from_secs(120)
2291        );
2292        assert_eq!(manager.rate_limit_config().message_queue_size, 256);
2293
2294        // Should allow when under limit
2295        assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
2296    }
2297
2298    #[tokio::test]
2299    async fn client_manager_builder_pattern() {
2300        let manager = ClientManager::new()
2301            .with_max_connections_per_ip(10)
2302            .with_timeout(Duration::from_secs(180))
2303            .with_message_queue_size(1024)
2304            .with_rate_limit_window(Duration::from_secs(90));
2305
2306        assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(10));
2307        assert_eq!(
2308            manager.rate_limit_config().client_timeout,
2309            Duration::from_secs(180)
2310        );
2311        assert_eq!(manager.rate_limit_config().message_queue_size, 1024);
2312        assert_eq!(
2313            manager.rate_limit_config().message_rate_window,
2314            Duration::from_secs(90)
2315        );
2316    }
2317
2318    // Integration test: Connection limits are enforced
2319    #[tokio::test]
2320    async fn connection_limit_enforcement_with_actual_clients() {
2321        let manager = ClientManager::new().with_max_connections_per_ip(2);
2322        let addr1 = create_test_socket_addr("192.168.1.1");
2323        let addr2 = create_test_socket_addr("192.168.1.2");
2324
2325        // First connection from IP1 should succeed
2326        let auth1 = create_test_auth_context("user-1", Limits::default());
2327        assert!(manager
2328            .check_connection_allowed(addr1, &Some(auth1.clone()))
2329            .await
2330            .is_ok());
2331
2332        // Simulate adding a client (we can't easily do this without a real WebSocket,
2333        // but we can verify the check logic works)
2334
2335        // Same IP, different auth context - should still count toward IP limit
2336        let auth2 = create_test_auth_context("user-2", Limits::default());
2337        assert!(manager
2338            .check_connection_allowed(addr1, &Some(auth2.clone()))
2339            .await
2340            .is_ok());
2341
2342        // Different IP - should succeed regardless
2343        let auth3 = create_test_auth_context("user-3", Limits::default());
2344        assert!(manager
2345            .check_connection_allowed(addr2, &Some(auth3.clone()))
2346            .await
2347            .is_ok());
2348    }
2349
2350    // Test subscription limit enforcement
2351    #[tokio::test]
2352    async fn subscription_limit_enforcement() {
2353        let manager = ClientManager::new();
2354        let addr = create_test_socket_addr("127.0.0.1");
2355
2356        // Create auth context with subscription limit
2357        let auth = create_test_auth_context(
2358            "user-1",
2359            Limits {
2360                max_subscriptions: Some(2),
2361                ..Default::default()
2362            },
2363        );
2364
2365        // Check should pass initially
2366        assert!(manager
2367            .check_connection_allowed(addr, &Some(auth.clone()))
2368            .await
2369            .is_ok());
2370
2371        // Note: We can't easily test the full subscription flow without a real connection,
2372        // but we verify the limit configuration is properly stored
2373        assert_eq!(auth.limits.max_subscriptions, Some(2));
2374    }
2375
2376    // Test snapshot limit enforcement
2377    #[tokio::test]
2378    async fn snapshot_limit_enforcement() {
2379        let manager = ClientManager::new();
2380        let addr = create_test_socket_addr("127.0.0.1");
2381
2382        let auth = create_test_auth_context(
2383            "user-1",
2384            Limits {
2385                max_snapshot_rows: Some(1000),
2386                ..Default::default()
2387            },
2388        );
2389
2390        assert!(manager
2391            .check_connection_allowed(addr, &Some(auth.clone()))
2392            .await
2393            .is_ok());
2394
2395        // Note: Actual snapshot limit checking happens in check_snapshot_allowed
2396        // which requires a connected client
2397    }
2398
2399    #[tokio::test]
2400    async fn two_consumers_share_the_account_connection_cap() {
2401        let manager = ClientManager::new();
2402        let account_limits = Limits {
2403            max_connections: Some(1),
2404            ..Limits::default()
2405        };
2406        let consumer_a = create_v2_auth_context(
2407            "consumer:a",
2408            "account:42",
2409            1,
2410            Limits::default(),
2411            account_limits.clone(),
2412        );
2413        insert_client(&manager, consumer_a);
2414
2415        // A different consumer on the same account is blocked by the
2416        // aggregate cap.
2417        let consumer_b = create_v2_auth_context(
2418            "consumer:b",
2419            "account:42",
2420            1,
2421            Limits::default(),
2422            account_limits.clone(),
2423        );
2424        let addr = create_test_socket_addr("127.0.0.1");
2425        assert!(manager
2426            .check_connection_allowed(addr, &Some(consumer_b))
2427            .await
2428            .is_err());
2429
2430        // A consumer on another account is unaffected.
2431        let other_account = create_v2_auth_context(
2432            "consumer:c",
2433            "account:43",
2434            1,
2435            Limits::default(),
2436            account_limits,
2437        );
2438        assert!(manager
2439            .check_connection_allowed(addr, &Some(other_account))
2440            .await
2441            .is_ok());
2442    }
2443
2444    #[tokio::test]
2445    async fn publishable_sessions_see_a_fixed_connection_limit_reason() {
2446        let manager = ClientManager::new();
2447        let account_limits = Limits {
2448            max_connections: Some(1),
2449            ..Limits::default()
2450        };
2451        insert_client(
2452            &manager,
2453            create_v2_auth_context(
2454                "consumer:a",
2455                "account:42",
2456                1,
2457                Limits::default(),
2458                account_limits.clone(),
2459            ),
2460        );
2461        let addr = create_test_socket_addr("127.0.0.1");
2462
2463        let visitor = create_v2_auth_context(
2464            "consumer:b",
2465            "account:42",
2466            1,
2467            Limits::default(),
2468            account_limits.clone(),
2469        );
2470        let deny = manager
2471            .check_connection_allowed(addr, &Some(visitor))
2472            .await
2473            .unwrap_err();
2474        assert_eq!(deny.code, AuthErrorCode::ConnectionLimitExceeded);
2475        let response = serde_json::to_string(&deny.to_error_response()).unwrap();
2476        assert_eq!(deny.reason, "Connection limit reached");
2477        for leaked in ["account:42", "consumer:b", "1 of 1"] {
2478            assert!(!response.contains(leaked), "{leaked} in {response}");
2479        }
2480
2481        // A secret-key session keeps the detailed reason.
2482        let mut server = create_v2_auth_context(
2483            "consumer:c",
2484            "account:42",
2485            1,
2486            Limits::default(),
2487            account_limits,
2488        );
2489        server.key_class = KeyClass::Secret;
2490        let deny = manager
2491            .check_connection_allowed(addr, &Some(server))
2492            .await
2493            .unwrap_err();
2494        assert!(
2495            deny.reason.contains("account account:42"),
2496            "{}",
2497            deny.reason
2498        );
2499    }
2500
2501    #[tokio::test]
2502    async fn one_consumer_is_limited_independently_of_its_account() {
2503        let manager = ClientManager::new();
2504        let limits = Limits {
2505            max_connections: Some(1),
2506            ..Limits::default()
2507        };
2508        let account_limits = Limits {
2509            max_connections: Some(10),
2510            ..Limits::default()
2511        };
2512        let consumer_a = create_v2_auth_context(
2513            "consumer:a",
2514            "account:42",
2515            1,
2516            limits.clone(),
2517            account_limits.clone(),
2518        );
2519        insert_client(&manager, consumer_a.clone());
2520
2521        let addr = create_test_socket_addr("127.0.0.1");
2522        // The same consumer hits its own cap.
2523        assert!(manager
2524            .check_connection_allowed(addr, &Some(consumer_a))
2525            .await
2526            .is_err());
2527
2528        // A sibling consumer under the same account is still admitted.
2529        let consumer_b =
2530            create_v2_auth_context("consumer:b", "account:42", 1, limits, account_limits);
2531        assert!(manager
2532            .check_connection_allowed(addr, &Some(consumer_b))
2533            .await
2534            .is_ok());
2535    }
2536
2537    #[tokio::test]
2538    async fn policy_version_upgrade_stale_and_conflict_rules_apply() {
2539        let manager = ClientManager::new();
2540        let addr = create_test_socket_addr("127.0.0.1");
2541        let limits_v1 = Limits {
2542            max_connections: Some(5),
2543            ..Limits::default()
2544        };
2545        let limits_v2 = Limits {
2546            max_connections: Some(2),
2547            ..Limits::default()
2548        };
2549
2550        // Version 1 admits and creates state.
2551        let v1 = create_v2_auth_context(
2552            "consumer:a",
2553            "account:42",
2554            1,
2555            Limits::default(),
2556            limits_v1.clone(),
2557        );
2558        assert!(manager
2559            .check_connection_allowed(addr, &Some(v1.clone()))
2560            .await
2561            .is_ok());
2562
2563        // Version 2 with new limits replaces the policy.
2564        let v2 = create_v2_auth_context(
2565            "consumer:a",
2566            "account:42",
2567            2,
2568            Limits::default(),
2569            limits_v2.clone(),
2570        );
2571        assert!(manager
2572            .check_connection_allowed(addr, &Some(v2))
2573            .await
2574            .is_ok());
2575
2576        // A stale version-1 token is rejected once version 2 was observed.
2577        let stale = manager.check_connection_allowed(addr, &Some(v1)).await;
2578        assert!(stale.is_err());
2579
2580        // Version 2 with different limits is a signing/config fault.
2581        let conflict =
2582            create_v2_auth_context("consumer:a", "account:42", 2, Limits::default(), limits_v1);
2583        assert!(manager
2584            .check_connection_allowed(addr, &Some(conflict))
2585            .await
2586            .is_err());
2587    }
2588
2589    #[tokio::test]
2590    async fn legacy_tokens_keep_old_behavior_and_are_counted() {
2591        let manager = ClientManager::new();
2592        let addr = create_test_socket_addr("127.0.0.1");
2593        let legacy = create_test_auth_context(
2594            "user-1",
2595            Limits {
2596                max_connections: Some(2),
2597                ..Limits::default()
2598            },
2599        );
2600        assert!(legacy.is_legacy_policy());
2601
2602        assert!(manager
2603            .check_connection_allowed(addr, &Some(legacy.clone()))
2604            .await
2605            .is_ok());
2606        assert_eq!(manager.account_policies().legacy_token_count(), 1);
2607        // Legacy tokens create no account policy state.
2608        assert_eq!(manager.account_policies().tracked_accounts(), 0);
2609
2610        // Legacy connection counting still keys off the subject.
2611        insert_client(&manager, legacy.clone());
2612        insert_client(&manager, legacy.clone());
2613        assert!(manager
2614            .check_connection_allowed(addr, &Some(legacy))
2615            .await
2616            .is_err());
2617    }
2618
2619    #[tokio::test]
2620    async fn account_message_budget_is_shared_across_consumers() {
2621        let manager = ClientManager::new();
2622        let account_limits = Limits {
2623            max_messages_per_minute: Some(2),
2624            ..Limits::default()
2625        };
2626        let client_a = insert_client(
2627            &manager,
2628            create_v2_auth_context(
2629                "consumer:a",
2630                "account:42",
2631                1,
2632                Limits::default(),
2633                account_limits.clone(),
2634            ),
2635        );
2636        let client_b = insert_client(
2637            &manager,
2638            create_v2_auth_context(
2639                "consumer:b",
2640                "account:42",
2641                1,
2642                Limits::default(),
2643                account_limits,
2644            ),
2645        );
2646
2647        assert!(manager.check_inbound_message_allowed(client_a).is_ok());
2648        assert!(manager.check_inbound_message_allowed(client_b).is_ok());
2649        // The third message anywhere on the account is rejected.
2650        assert!(manager.check_inbound_message_allowed(client_a).is_err());
2651    }
2652
2653    #[tokio::test]
2654    async fn account_state_evicts_when_idle_and_unreferenced() {
2655        let manager = ClientManager::new();
2656        let addr = create_test_socket_addr("127.0.0.1");
2657        let context = create_v2_auth_context(
2658            "consumer:a",
2659            "account:42",
2660            1,
2661            Limits::default(),
2662            Limits::default(),
2663        );
2664        assert!(manager
2665            .check_connection_allowed(addr, &Some(context))
2666            .await
2667            .is_ok());
2668        assert_eq!(manager.account_policies().tracked_accounts(), 1);
2669
2670        // No live connection references the account; an idle sweep with a
2671        // zero TTL registry drops it.
2672        manager.account_policies().evict_idle(|_| false);
2673        assert_eq!(
2674            manager.account_policies().tracked_accounts(),
2675            1,
2676            "TTL keeps fresh entries"
2677        );
2678        manager.cleanup_account_state();
2679        assert_eq!(
2680            manager.account_policies().tracked_accounts(),
2681            1,
2682            "fresh entries survive sweep"
2683        );
2684    }
2685
2686    #[test]
2687    fn expired_token_is_removed_without_deadlocking() {
2688        let manager = ClientManager::new();
2689        let mut context = create_test_auth_context("user-1", Limits::default());
2690        context.expires_at = 1;
2691        let client_id = insert_client(&manager, context);
2692
2693        // Send from another thread: a deadlock blocks that thread for good,
2694        // so the test fails on the timeout instead of hanging.
2695        let (done_tx, done_rx) = std::sync::mpsc::channel();
2696        let sender = manager.clone();
2697        std::thread::spawn(move || {
2698            let result = sender.send_to_client(client_id, Arc::new(Bytes::from_static(b"update")));
2699            let _ = done_tx.send(result);
2700        });
2701
2702        let result = done_rx
2703            .recv_timeout(Duration::from_secs(5))
2704            .expect("send_to_client deadlocked on an expired token");
2705        assert_eq!(result, Err(SendError::ClientDisconnected));
2706        assert!(!manager.has_client(client_id));
2707    }
2708
2709    #[tokio::test]
2710    async fn an_expired_client_keeps_its_close_reason_when_its_queue_is_full() {
2711        let manager = ClientManager::new();
2712        let (sender, mut queue) = mpsc::channel(1);
2713        sender
2714            .try_send(Message::Text("backlog".into()))
2715            .expect("room for one message");
2716        let client_id = Uuid::new_v4();
2717        let mut context = create_test_auth_context("user-1", Limits::default());
2718        context.expires_at = 1;
2719        let client = ClientInfo::new(
2720            client_id,
2721            sender,
2722            Some(context),
2723            create_test_socket_addr("127.0.0.1"),
2724        );
2725        let close_frame = client.close_frame.clone();
2726        manager.clients.insert(client_id, client);
2727
2728        assert!(manager.check_and_remove_expired(client_id));
2729
2730        let frame = close_frame
2731            .get()
2732            .expect("the close reason is recorded even though the queue is full");
2733        assert_eq!(frame.code, CloseCode::Policy);
2734        assert_eq!(
2735            frame.reason.as_str(),
2736            "token-expired: Authentication token expired"
2737        );
2738        // The backlog is still delivered first, and removing the client
2739        // dropped its sender, so the queue then ends and the sender task
2740        // closes the socket with the recorded reason.
2741        assert!(matches!(queue.recv().await, Some(Message::Text(_))));
2742        assert!(queue.recv().await.is_none());
2743    }
2744
2745    #[test]
2746    fn unexpired_token_is_kept() {
2747        let manager = ClientManager::new();
2748        let client_id = insert_client(
2749            &manager,
2750            create_test_auth_context("user-1", Limits::default()),
2751        );
2752
2753        assert!(!manager.check_and_remove_expired(client_id));
2754        assert!(manager.has_client(client_id));
2755    }
2756
2757    /// Every eviction path removes the client from the registry, and leaving
2758    /// the registry must end the client's subscriptions: a task left running
2759    /// for an evicted client never learns it is gone if it never sends.
2760    #[tokio::test]
2761    async fn eviction_cancels_every_subscription_of_the_evicted_client() {
2762        async fn subscribed(
2763            manager: &ClientManager,
2764            context: AuthContext,
2765        ) -> (Uuid, Vec<CancellationToken>) {
2766            let client_id = insert_client(manager, context);
2767            let connection = manager.connection_token(client_id);
2768            let derived = connection.child_token();
2769            let registered = CancellationToken::new();
2770            assert!(
2771                manager
2772                    .add_client_subscription(client_id, "derived".into(), derived.clone())
2773                    .await
2774            );
2775            assert!(
2776                manager
2777                    .add_client_subscription(client_id, "registered".into(), registered.clone())
2778                    .await
2779            );
2780            (client_id, vec![connection, derived, registered])
2781        }
2782
2783        // Stale: idle past the client timeout.
2784        let manager = ClientManager::new().with_timeout(Duration::ZERO);
2785        let (client_id, tokens) = subscribed(
2786            &manager,
2787            create_test_auth_context("user-1", Limits::default()),
2788        )
2789        .await;
2790        std::thread::sleep(Duration::from_millis(2));
2791        assert_eq!(manager.cleanup_stale_clients(), 1);
2792        assert!(!manager.has_client(client_id));
2793        assert!(
2794            tokens.iter().all(CancellationToken::is_cancelled),
2795            "stale eviction"
2796        );
2797
2798        // Expired token.
2799        let manager = ClientManager::new();
2800        let mut expired = create_test_auth_context("user-2", Limits::default());
2801        expired.expires_at = 1;
2802        let (client_id, tokens) = subscribed(&manager, expired).await;
2803        assert!(manager.check_and_remove_expired(client_id));
2804        assert!(
2805            tokens.iter().all(CancellationToken::is_cancelled),
2806            "expiry eviction"
2807        );
2808
2809        // Backpressure: the outbound queue is full.
2810        let manager = ClientManager::new();
2811        let (client_id, tokens) = subscribed(
2812            &manager,
2813            create_test_auth_context("user-3", Limits::default()),
2814        )
2815        .await;
2816        let payload = Arc::new(Bytes::from_static(b"{}"));
2817        while manager.send_to_client(client_id, payload.clone()).is_ok() {}
2818        assert!(!manager.has_client(client_id));
2819        assert!(
2820            tokens.iter().all(CancellationToken::is_cancelled),
2821            "backpressure eviction"
2822        );
2823
2824        // An unknown client has no live connection to hang work off.
2825        assert!(manager.connection_token(Uuid::new_v4()).is_cancelled());
2826    }
2827
2828    /// A refresh installs a new identity on a live socket, so it cannot move
2829    /// the socket into an account already at its connection cap. The socket
2830    /// is not counted against itself: refreshing within its own identity at
2831    /// the cap still succeeds.
2832    #[tokio::test]
2833    async fn refresh_cannot_move_a_socket_into_an_account_at_its_connection_cap() {
2834        let manager = ClientManager::new();
2835        let capped = Limits {
2836            max_connections: Some(1),
2837            ..Limits::default()
2838        };
2839        let into_capped = || {
2840            create_v2_auth_context(
2841                "consumer:b",
2842                "account:b",
2843                1,
2844                Limits::default(),
2845                capped.clone(),
2846            )
2847        };
2848        let holder = insert_client(&manager, into_capped());
2849        let other = insert_client(
2850            &manager,
2851            create_v2_auth_context(
2852                "consumer:c",
2853                "account:c",
2854                1,
2855                Limits::default(),
2856                Limits::default(),
2857            ),
2858        );
2859
2860        let deny = manager
2861            .try_update_client_auth_async(other, into_capped())
2862            .await
2863            .unwrap_err();
2864        assert_eq!(deny.code, AuthErrorCode::ConnectionLimitExceeded);
2865        assert_eq!(
2866            manager
2867                .get_auth_context(other)
2868                .unwrap()
2869                .account_key
2870                .as_deref(),
2871            Some("account:c"),
2872            "a refused refresh keeps the current identity"
2873        );
2874        assert!(manager
2875            .try_update_client_auth(other, into_capped())
2876            .is_err());
2877
2878        assert!(manager
2879            .try_update_client_auth_async(holder, into_capped())
2880            .await
2881            .unwrap());
2882    }
2883
2884    /// The per-consumer cap applies the same way.
2885    #[tokio::test]
2886    async fn refresh_cannot_move_a_socket_into_a_consumer_at_its_connection_cap() {
2887        let manager = ClientManager::new();
2888        let capped = || {
2889            create_v2_auth_context(
2890                "consumer:b",
2891                "account:a",
2892                1,
2893                Limits {
2894                    max_connections: Some(1),
2895                    ..Limits::default()
2896                },
2897                Limits::default(),
2898            )
2899        };
2900        insert_client(&manager, capped());
2901        let other = insert_client(
2902            &manager,
2903            create_v2_auth_context(
2904                "consumer:c",
2905                "account:a",
2906                1,
2907                Limits::default(),
2908                Limits::default(),
2909            ),
2910        );
2911
2912        let deny = manager
2913            .try_update_client_auth_async(other, capped())
2914            .await
2915            .unwrap_err();
2916        assert_eq!(deny.code, AuthErrorCode::ConnectionLimitExceeded);
2917    }
2918
2919    /// After a newer policy version is observed, refreshing into an older one
2920    /// is refused as the handshake would refuse it.
2921    #[tokio::test]
2922    async fn refresh_into_a_stale_policy_version_is_refused() {
2923        let manager = ClientManager::new();
2924        let addr = create_test_socket_addr("127.0.0.1");
2925        let version = |policy_version: u32, max_connections: u32| {
2926            create_v2_auth_context(
2927                "consumer:a",
2928                "account:a",
2929                policy_version,
2930                Limits::default(),
2931                Limits {
2932                    max_connections: Some(max_connections),
2933                    ..Limits::default()
2934                },
2935            )
2936        };
2937        manager
2938            .check_connection_allowed(addr, &Some(version(2, 2)))
2939            .await
2940            .unwrap();
2941        let client_id = insert_client(&manager, version(2, 2));
2942
2943        let deny = manager
2944            .try_update_client_auth_async(client_id, version(1, 50))
2945            .await
2946            .unwrap_err();
2947        assert_eq!(deny.code, AuthErrorCode::TokenExpired);
2948        assert_eq!(
2949            manager.get_auth_context(client_id).unwrap().policy_version,
2950            Some(2)
2951        );
2952    }
2953
2954    /// A refresh must not install a subscription cap the socket already exceeds.
2955    #[tokio::test]
2956    async fn refresh_refuses_a_token_whose_subscription_cap_is_below_open_subscriptions() {
2957        let manager = ClientManager::new();
2958        let with_cap = |max_subscriptions: u32| {
2959            create_test_auth_context(
2960                "user-1",
2961                Limits {
2962                    max_subscriptions: Some(max_subscriptions),
2963                    ..Limits::default()
2964                },
2965            )
2966        };
2967        let client_id = insert_client(&manager, with_cap(5));
2968        for id in ["a", "b", "c"] {
2969            assert!(
2970                manager
2971                    .add_client_subscription(client_id, id.into(), CancellationToken::new())
2972                    .await
2973            );
2974        }
2975
2976        let deny = manager
2977            .try_update_client_auth_async(client_id, with_cap(2))
2978            .await
2979            .unwrap_err();
2980        assert_eq!(deny.code, AuthErrorCode::SubscriptionLimitExceeded);
2981        assert_eq!(
2982            manager
2983                .get_auth_context(client_id)
2984                .unwrap()
2985                .limits
2986                .max_subscriptions,
2987            Some(5)
2988        );
2989        assert!(manager
2990            .try_update_client_auth_async(client_id, with_cap(3))
2991            .await
2992            .unwrap());
2993    }
2994
2995    // Test WebSocketRateLimiter integration
2996    #[tokio::test]
2997    async fn test_rate_limiter_integration() {
2998        use crate::websocket::rate_limiter::{RateLimiterConfig, WebSocketRateLimiter};
2999
3000        let rate_limiter = Arc::new(WebSocketRateLimiter::new(RateLimiterConfig::default()));
3001        let manager = ClientManager::new().with_rate_limiter(rate_limiter);
3002        let addr = create_test_socket_addr("127.0.0.1");
3003
3004        // Should allow connections when rate limiter is configured
3005        let auth = create_test_auth_context("user-1", Limits::default());
3006        assert!(manager
3007            .check_connection_allowed(addr, &Some(auth))
3008            .await
3009            .is_ok());
3010    }
3011}