1use std::sync::Arc;
34use std::time::{Duration, SystemTime};
35
36use atd_runtime::secrets::{BearerIdentity, BrokerError, TokenBroker};
37use tokio::sync::mpsc;
38use tokio::task::JoinHandle;
39
40pub const DEFAULT_REFRESH_CADENCE: Duration = Duration::from_secs(60);
43
44const CHANNEL_BUFFER: usize = 8;
48
49#[derive(Debug, Clone)]
51#[non_exhaustive]
52pub enum RefreshEvent {
53 Refreshed {
59 caps: Vec<String>,
60 expires_at: Option<SystemTime>,
61 },
62 AuthLost { reason: AuthLostReason },
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
73#[non_exhaustive]
74pub enum AuthLostReason {
75 Expired,
76 Revoked(String),
77 Unknown,
81 BrokerError {
86 reason: String,
87 transient: bool,
88 },
89}
90
91pub fn spawn_bearer_refresh(
102 broker: Arc<dyn TokenBroker>,
103 bearer: String,
104 initial: BearerIdentity,
105 cadence: Duration,
106) -> (JoinHandle<()>, mpsc::Receiver<RefreshEvent>) {
107 let (tx, rx) = mpsc::channel(CHANNEL_BUFFER);
108
109 let handle = tokio::spawn(async move {
110 let mut current_expires_at = initial.expires_at;
111 loop {
112 let sleep_for = next_sleep_duration(cadence, current_expires_at);
113 tokio::time::sleep(sleep_for).await;
114
115 match broker.resolve_bearer(&bearer).await {
116 Ok(Some(id)) => {
117 current_expires_at = id.expires_at;
118 let evt = RefreshEvent::Refreshed {
119 caps: id.granted_capabilities,
120 expires_at: id.expires_at,
121 };
122 if tx.send(evt).await.is_err() {
123 break;
125 }
126 }
127 Ok(None) => {
128 let _ = tx
129 .send(RefreshEvent::AuthLost {
130 reason: AuthLostReason::Unknown,
131 })
132 .await;
133 break;
134 }
135 Err(BrokerError::Expired) => {
136 let _ = tx
137 .send(RefreshEvent::AuthLost {
138 reason: AuthLostReason::Expired,
139 })
140 .await;
141 break;
142 }
143 Err(BrokerError::Revoked(msg)) => {
144 let _ = tx
145 .send(RefreshEvent::AuthLost {
146 reason: AuthLostReason::Revoked(msg),
147 })
148 .await;
149 break;
150 }
151 Err(BrokerError::Lookup(msg)) => {
152 let _ = tx
157 .send(RefreshEvent::AuthLost {
158 reason: AuthLostReason::BrokerError {
159 reason: msg,
160 transient: true,
161 },
162 })
163 .await;
164 break;
165 }
166 Err(BrokerError::Internal(msg)) => {
167 let _ = tx
168 .send(RefreshEvent::AuthLost {
169 reason: AuthLostReason::BrokerError {
170 reason: msg,
171 transient: false,
172 },
173 })
174 .await;
175 break;
176 }
177 Err(BrokerError::NotConfigured) => {
178 let _ = tx
179 .send(RefreshEvent::AuthLost {
180 reason: AuthLostReason::BrokerError {
181 reason: "broker NotConfigured".into(),
182 transient: false,
183 },
184 })
185 .await;
186 break;
187 }
188 }
189 }
190 });
191
192 (handle, rx)
193}
194
195fn next_sleep_duration(cadence: Duration, expires_at: Option<SystemTime>) -> Duration {
200 match expires_at {
201 Some(t) => {
202 let now = SystemTime::now();
203 match t.duration_since(now) {
204 Ok(until_expiry) => cadence.min(until_expiry),
205 Err(_) => cadence, }
207 }
208 None => cadence,
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use atd_runtime::secrets::{ResolveBearerFuture, ResolveFuture};
216 use std::sync::Mutex;
217
218 #[derive(Debug, Default)]
223 struct MutBroker {
224 state: Mutex<MutState>,
225 }
226 #[derive(Default)]
227 enum MutState {
228 #[default]
229 Ok,
230 OkShrunk,
231 Expired,
232 Revoked(String),
233 Unknown,
234 Internal(String),
235 Lookup(String),
236 }
237 impl std::fmt::Debug for MutState {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 match self {
241 Self::Ok => write!(f, "Ok"),
242 Self::OkShrunk => write!(f, "OkShrunk"),
243 Self::Expired => write!(f, "Expired"),
244 Self::Revoked(s) => write!(f, "Revoked({s})"),
245 Self::Unknown => write!(f, "Unknown"),
246 Self::Internal(s) => write!(f, "Internal({s})"),
247 Self::Lookup(s) => write!(f, "Lookup({s})"),
248 }
249 }
250 }
251 impl MutBroker {
252 fn set(&self, st: MutState) {
253 *self.state.lock().unwrap() = st;
254 }
255 }
256 impl TokenBroker for MutBroker {
257 fn resolve<'a>(&'a self, _caller_id: Option<&'a str>) -> ResolveFuture<'a> {
258 Box::pin(async { Ok(None) })
259 }
260 fn resolve_bearer<'a>(&'a self, _bearer: &'a str) -> ResolveBearerFuture<'a> {
261 let st = std::mem::take(&mut *self.state.lock().unwrap());
262 *self.state.lock().unwrap() = match &st {
266 MutState::Ok => MutState::Ok,
267 MutState::OkShrunk => MutState::OkShrunk,
268 _ => MutState::default(),
269 };
270 let snapshot = st;
272 Box::pin(async move {
273 match snapshot {
274 MutState::Ok => Ok(Some(BearerIdentity {
275 caller_id: "agent-A".into(),
276 granted_capabilities: vec!["records:read".into(), "summary:read".into()],
277 secrets: None,
278 expires_at: None,
279 cache_until: None,
280 })),
281 MutState::OkShrunk => Ok(Some(BearerIdentity {
282 caller_id: "agent-A".into(),
283 granted_capabilities: vec!["records:read".into()],
284 secrets: None,
285 expires_at: None,
286 cache_until: None,
287 })),
288 MutState::Expired => Err(BrokerError::Expired),
289 MutState::Revoked(s) => Err(BrokerError::Revoked(s)),
290 MutState::Unknown => Ok(None),
291 MutState::Internal(s) => Err(BrokerError::Internal(s)),
292 MutState::Lookup(s) => Err(BrokerError::Lookup(s)),
293 }
294 })
295 }
296 }
297
298 fn ok_identity() -> BearerIdentity {
299 BearerIdentity {
300 caller_id: "agent-A".into(),
301 granted_capabilities: vec!["records:read".into(), "summary:read".into()],
302 secrets: None,
303 expires_at: None,
304 cache_until: None,
305 }
306 }
307
308 #[test]
311 fn no_expires_at_sleeps_full_cadence() {
312 let dur = next_sleep_duration(Duration::from_secs(60), None);
313 assert_eq!(dur, Duration::from_secs(60));
314 }
315
316 #[test]
317 fn expires_at_in_the_past_returns_cadence_not_zero() {
318 let past = SystemTime::now() - Duration::from_secs(100);
319 let dur = next_sleep_duration(Duration::from_secs(60), Some(past));
320 assert_eq!(dur, Duration::from_secs(60));
321 }
322
323 #[test]
324 fn expires_at_sooner_than_cadence_clamps_sleep() {
325 let soon = SystemTime::now() + Duration::from_secs(5);
326 let dur = next_sleep_duration(Duration::from_secs(60), Some(soon));
327 assert!(dur < Duration::from_secs(7));
329 assert!(dur > Duration::from_secs(3));
330 }
331
332 #[test]
333 fn expires_at_far_in_future_keeps_cadence() {
334 let far = SystemTime::now() + Duration::from_secs(3600);
335 let dur = next_sleep_duration(Duration::from_secs(60), Some(far));
336 assert_eq!(dur, Duration::from_secs(60));
337 }
338
339 #[tokio::test]
344 async fn happy_refresh_emits_refreshed_with_current_caps() {
345 let broker: Arc<dyn TokenBroker> = Arc::new(MutBroker::default());
346 let (handle, mut rx) = spawn_bearer_refresh(
347 broker.clone(),
348 "tok".into(),
349 ok_identity(),
350 Duration::from_millis(50),
351 );
352
353 let evt = tokio::time::timeout(Duration::from_millis(500), rx.recv())
354 .await
355 .expect("event within 500ms")
356 .expect("non-closed channel");
357
358 match evt {
359 RefreshEvent::Refreshed { caps, .. } => {
360 assert!(caps.contains(&"records:read".to_string()));
361 assert!(caps.contains(&"summary:read".to_string()));
362 }
363 other => panic!("expected Refreshed, got {other:?}"),
364 }
365 handle.abort();
366 }
367
368 #[tokio::test]
369 async fn shrunk_caps_propagate_via_refreshed_event() {
370 let broker_concrete = Arc::new(MutBroker::default());
371 broker_concrete.set(MutState::OkShrunk);
372 let broker: Arc<dyn TokenBroker> = broker_concrete.clone();
373
374 let (handle, mut rx) = spawn_bearer_refresh(
375 broker,
376 "tok".into(),
377 ok_identity(), Duration::from_millis(50),
379 );
380
381 let evt = tokio::time::timeout(Duration::from_millis(500), rx.recv())
382 .await
383 .expect("event within 500ms")
384 .expect("non-closed channel");
385
386 match evt {
387 RefreshEvent::Refreshed { caps, .. } => {
388 assert_eq!(caps, vec!["records:read".to_string()]);
389 }
390 other => panic!("expected Refreshed(shrunk), got {other:?}"),
391 }
392 handle.abort();
393 }
394
395 #[tokio::test]
396 async fn mid_stream_revoke_emits_auth_lost_revoked_and_closes_channel() {
397 let broker_concrete = Arc::new(MutBroker::default());
398 let broker: Arc<dyn TokenBroker> = broker_concrete.clone();
399
400 let (handle, mut rx) = spawn_bearer_refresh(
401 broker,
402 "tok".into(),
403 ok_identity(),
404 Duration::from_millis(50),
405 );
406
407 let evt1 = tokio::time::timeout(Duration::from_millis(500), rx.recv())
409 .await
410 .expect("first event")
411 .expect("non-closed");
412 assert!(matches!(evt1, RefreshEvent::Refreshed { .. }));
413
414 broker_concrete.set(MutState::Revoked("user revoked".into()));
416
417 let evt2 = tokio::time::timeout(Duration::from_millis(500), rx.recv())
418 .await
419 .expect("second event")
420 .expect("non-closed before AuthLost");
421 match evt2 {
422 RefreshEvent::AuthLost {
423 reason: AuthLostReason::Revoked(msg),
424 } => assert_eq!(msg, "user revoked"),
425 other => panic!("expected AuthLost(Revoked), got {other:?}"),
426 }
427
428 let evt3 = tokio::time::timeout(Duration::from_millis(500), rx.recv())
430 .await
431 .expect("channel-close detection within 500ms");
432 assert!(evt3.is_none(), "channel must close after AuthLost");
433
434 let _ = handle.await;
436 }
437
438 #[tokio::test]
439 async fn mid_stream_expired_emits_auth_lost_expired() {
440 let broker_concrete = Arc::new(MutBroker::default());
441 broker_concrete.set(MutState::Expired);
442 let broker: Arc<dyn TokenBroker> = broker_concrete.clone();
443
444 let (handle, mut rx) = spawn_bearer_refresh(
445 broker,
446 "tok".into(),
447 ok_identity(),
448 Duration::from_millis(50),
449 );
450
451 let evt = tokio::time::timeout(Duration::from_millis(500), rx.recv())
452 .await
453 .expect("event within 500ms")
454 .expect("non-closed");
455 assert!(matches!(
456 evt,
457 RefreshEvent::AuthLost {
458 reason: AuthLostReason::Expired
459 }
460 ));
461 handle.abort();
462 }
463
464 #[tokio::test]
465 async fn broker_internal_error_emits_persistent_auth_lost() {
466 let broker_concrete = Arc::new(MutBroker::default());
467 broker_concrete.set(MutState::Internal("oh no".into()));
468 let broker: Arc<dyn TokenBroker> = broker_concrete.clone();
469
470 let (handle, mut rx) = spawn_bearer_refresh(
471 broker,
472 "tok".into(),
473 ok_identity(),
474 Duration::from_millis(50),
475 );
476
477 let evt = tokio::time::timeout(Duration::from_millis(500), rx.recv())
478 .await
479 .expect("event")
480 .expect("non-closed");
481 match evt {
482 RefreshEvent::AuthLost {
483 reason: AuthLostReason::BrokerError { transient, .. },
484 } => assert!(!transient),
485 other => panic!("expected persistent BrokerError, got {other:?}"),
486 }
487 handle.abort();
488 }
489
490 #[tokio::test]
491 async fn broker_lookup_error_emits_transient_auth_lost() {
492 let broker_concrete = Arc::new(MutBroker::default());
493 broker_concrete.set(MutState::Lookup("sqlite locked".into()));
494 let broker: Arc<dyn TokenBroker> = broker_concrete.clone();
495
496 let (handle, mut rx) = spawn_bearer_refresh(
497 broker,
498 "tok".into(),
499 ok_identity(),
500 Duration::from_millis(50),
501 );
502
503 let evt = tokio::time::timeout(Duration::from_millis(500), rx.recv())
504 .await
505 .expect("event")
506 .expect("non-closed");
507 match evt {
508 RefreshEvent::AuthLost {
509 reason: AuthLostReason::BrokerError { transient, .. },
510 } => assert!(transient),
511 other => panic!("expected transient BrokerError, got {other:?}"),
512 }
513 handle.abort();
514 }
515
516 #[tokio::test]
517 async fn unknown_bearer_emits_auth_lost_unknown() {
518 let broker_concrete = Arc::new(MutBroker::default());
519 broker_concrete.set(MutState::Unknown);
520 let broker: Arc<dyn TokenBroker> = broker_concrete.clone();
521
522 let (handle, mut rx) = spawn_bearer_refresh(
523 broker,
524 "tok".into(),
525 ok_identity(),
526 Duration::from_millis(50),
527 );
528
529 let evt = tokio::time::timeout(Duration::from_millis(500), rx.recv())
530 .await
531 .expect("event")
532 .expect("non-closed");
533 assert!(matches!(
534 evt,
535 RefreshEvent::AuthLost {
536 reason: AuthLostReason::Unknown
537 }
538 ));
539 handle.abort();
540 }
541
542 #[tokio::test]
543 async fn dropping_receiver_aborts_refresh_loop() {
544 let broker: Arc<dyn TokenBroker> = Arc::new(MutBroker::default());
545 let (handle, rx) = spawn_bearer_refresh(
546 broker,
547 "tok".into(),
548 ok_identity(),
549 Duration::from_millis(50),
550 );
551
552 drop(rx);
554 let exited = tokio::time::timeout(Duration::from_millis(500), handle).await;
556 assert!(
557 exited.is_ok(),
558 "task must exit within ~1 cadence after receiver drop"
559 );
560 }
561}