From 4923fa89e396d92cee9daff2b84364eac31d6033 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 7 Jun 2026 20:42:08 +0200 Subject: [PATCH] fix(manager-core): F-S-008 TTL + invalidate hook on realtime_authority key cache key_cache: Mutex>> was populated on first read and never evicted, bounded, or cleared on app deletion. Two problems: (1) Once key rotation lands, every running process keeps accepting tokens signed by the old key until restart. (2) Dropping and re-creating an app (FK CASCADE removes app_secrets, fresh row inserted) made the cache hand out the OLD key bytes. - Wrap the cache value as `(Instant, Vec)`; entries expire after KEY_CACHE_TTL = 5min. - Stale reads fall through to the secrets repo (no error, just slower). - New `invalidate_signing_key(app_id)` for explicit eviction once the app-secrets rotation path lands. AUDIT.md anchor: F-S-008. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/manager-core/src/realtime_authority.rs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) 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]