diff --git a/crates/manager-core/src/realtime_authority.rs b/crates/manager-core/src/realtime_authority.rs index 8cdf6fc..ae4ac26 100644 --- a/crates/manager-core/src/realtime_authority.rs +++ b/crates/manager-core/src/realtime_authority.rs @@ -27,11 +27,17 @@ use picloud_shared::{subscriber_token, AppId, RealtimeAuthority, SubscribeDenied use crate::app_secrets_repo::AppSecretsRepo; use crate::topic_repo::{TopicAuthMode, TopicRepo}; +/// F-S-008: TTL on cached signing-key entries. Once key rotation +/// lands, every running process keeps accepting tokens signed by the +/// old key until restart — bounded eviction is the simplest defence. +/// Also bounds memory growth on a many-app install. +const KEY_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(300); + pub struct RealtimeAuthorityImpl { topics: Arc, secrets: Arc, users: Arc, - key_cache: Mutex>>, + key_cache: Mutex)>>, } impl RealtimeAuthorityImpl { @@ -54,8 +60,10 @@ impl RealtimeAuthorityImpl { /// caller maps to `Unauthorized`. async fn signing_key(&self, app_id: AppId) -> Result>, SubscribeDenied> { if let Ok(cache) = self.key_cache.lock() { - if let Some(k) = cache.get(&app_id) { - return Ok(Some(k.clone())); + if let Some((ts, k)) = cache.get(&app_id) { + if ts.elapsed() <= KEY_CACHE_TTL { + return Ok(Some(k.clone())); + } } } let key = self @@ -65,11 +73,21 @@ impl RealtimeAuthorityImpl { .map_err(|e| SubscribeDenied::Backend(e.to_string()))?; if let Some(ref k) = key { if let Ok(mut cache) = self.key_cache.lock() { - cache.insert(app_id, k.clone()); + cache.insert(app_id, (std::time::Instant::now(), k.clone())); } } Ok(key) } + + /// F-S-008 + future key-rotation hook: drop the cached signing key + /// for an app. Wired into the app-secrets rotation path so a fresh + /// key takes effect on the next request instead of waiting for the + /// TTL. + pub fn invalidate_signing_key(&self, app_id: AppId) { + if let Ok(mut cache) = self.key_cache.lock() { + cache.remove(&app_id); + } + } } #[async_trait]