Skip to main content

car_auth/
credential_read.rs

1use car_secrets::SecretError;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
7use std::sync::{Arc, Mutex as StdMutex, OnceLock, Weak};
8use tokio::sync::{mpsc, watch, Mutex};
9
10const CREDENTIAL_READ_EVENT_QUEUE_CAPACITY: usize = 4;
11
12/// Whether an authoritative credential use may start a new physical read.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CredentialReadMode {
15    /// Join an in-flight read or start one when the coordinator is not cooling
16    /// down from a terminal store failure.
17    Use,
18    /// Join an in-flight read or explicitly clear cooldown and start one new
19    /// generation.
20    Retry,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub(crate) enum CredentialReadPurpose {
25    Resolve,
26    AuthoritativeResolve,
27    ForceRefresh,
28    /// Automatic recovery for one rejected bearer. The secret-free digest is
29    /// carried by the flight so a waiter cannot accept or attribute another
30    /// credential's in-flight refresh result.
31    AutomaticRefresh(super::RejectedBearerKey),
32}
33
34/// Parslee request authority resolved from one credential snapshot.
35#[derive(Clone, PartialEq, Eq)]
36pub struct ResolvedParsleeCredential {
37    pub access_token: String,
38    pub api_base: String,
39    pub expires_at: u64,
40}
41
42impl std::fmt::Debug for ResolvedParsleeCredential {
43    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        formatter
45            .debug_struct("ResolvedParsleeCredential")
46            .field("access_token", &"[REDACTED]")
47            .field("api_base", &self.api_base)
48            .field("expires_at", &self.expires_at)
49            .finish()
50    }
51}
52
53/// Stable recovery classes. No variant contains secret material.
54#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
55#[serde(rename_all = "snake_case")]
56pub enum CredentialReadFailureKind {
57    Denied,
58    Cancelled,
59    TimedOut,
60    Unreadable,
61    Cooldown,
62}
63
64/// One terminal authoritative-read failure.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct CredentialReadError {
67    pub kind: CredentialReadFailureKind,
68    pub message: String,
69}
70
71impl std::fmt::Display for CredentialReadError {
72    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        formatter.write_str(&self.message)
74    }
75}
76
77impl std::error::Error for CredentialReadError {}
78
79impl From<SecretError> for CredentialReadError {
80    fn from(error: SecretError) -> Self {
81        let kind = match error {
82            SecretError::AccessDenied { .. } => CredentialReadFailureKind::Denied,
83            SecretError::UserCancelled { .. } => CredentialReadFailureKind::Cancelled,
84            SecretError::HelperTimedOut { .. } => CredentialReadFailureKind::TimedOut,
85            SecretError::Unavailable(_)
86            | SecretError::NotFound { .. }
87            | SecretError::Backend(_)
88            | SecretError::InvalidJson(_) => CredentialReadFailureKind::Unreadable,
89        };
90        Self {
91            kind,
92            message: error.to_string(),
93        }
94    }
95}
96
97/// Secret-free state suitable for daemon event fanout.
98#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
99#[serde(rename_all = "snake_case")]
100pub enum CredentialReadStatusState {
101    Pending,
102    Configured,
103    SignedOut,
104    Denied,
105    Cancelled,
106    TimedOut,
107    Unreadable,
108}
109
110/// Latest process-owned credential-read generation and its public state.
111#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
112pub struct CredentialReadStatus {
113    pub generation: u64,
114    pub state: CredentialReadStatusState,
115}
116
117/// Why an ordered credential-event subscription stopped.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum CredentialReadEventCloseReason {
120    /// The bounded subscriber queue filled before its consumer kept pace.
121    Lagged,
122    /// The process-owned publisher closed unexpectedly.
123    Closed,
124}
125
126struct CredentialReadEventSubscribers {
127    state: StdMutex<CredentialReadEventSubscriberState>,
128}
129
130struct CredentialReadEventSubscriberState {
131    next_id: u64,
132    latest: Option<CredentialReadStatus>,
133    subscribers: HashMap<u64, CredentialReadEventSubscriber>,
134}
135
136struct CredentialReadEventSubscriber {
137    sender: mpsc::Sender<CredentialReadStatus>,
138    close_reason: watch::Sender<Option<CredentialReadEventCloseReason>>,
139}
140
141impl CredentialReadEventSubscribers {
142    fn new() -> Self {
143        Self {
144            state: StdMutex::new(CredentialReadEventSubscriberState {
145                next_id: 0,
146                latest: None,
147                subscribers: HashMap::new(),
148            }),
149        }
150    }
151
152    fn subscribe(self: &Arc<Self>) -> CredentialReadEventSubscription {
153        self.subscribe_with_snapshot().events
154    }
155
156    fn subscribe_with_snapshot(self: &Arc<Self>) -> CredentialReadEventHandoff {
157        let (sender, receiver) = mpsc::channel(CREDENTIAL_READ_EVENT_QUEUE_CAPACITY);
158        let (close_reason, close_updates) = watch::channel(None);
159        let mut state = self
160            .state
161            .lock()
162            .expect("credential event subscribers mutex poisoned");
163        let snapshot = state.latest;
164        let id = state.next_id;
165        state.next_id = state
166            .next_id
167            .checked_add(1)
168            .expect("credential event subscription id exhausted");
169        state.subscribers.insert(
170            id,
171            CredentialReadEventSubscriber {
172                sender,
173                close_reason,
174            },
175        );
176        CredentialReadEventHandoff {
177            snapshot,
178            events: CredentialReadEventSubscription {
179                id,
180                receiver,
181                close_updates,
182                subscribers: Arc::downgrade(self),
183            },
184        }
185    }
186
187    fn publish(&self, status: CredentialReadStatus) {
188        let mut state = self
189            .state
190            .lock()
191            .expect("credential event subscribers mutex poisoned");
192        state.latest = Some(status);
193        state
194            .subscribers
195            .retain(|_, subscriber| match subscriber.sender.try_send(status) {
196                Ok(()) => true,
197                Err(mpsc::error::TrySendError::Full(_)) => {
198                    subscriber
199                        .close_reason
200                        .send_replace(Some(CredentialReadEventCloseReason::Lagged));
201                    false
202                }
203                Err(mpsc::error::TrySendError::Closed(_)) => false,
204            });
205    }
206
207    fn unsubscribe(&self, id: u64) {
208        self.state
209            .lock()
210            .expect("credential event subscribers mutex poisoned")
211            .subscribers
212            .remove(&id);
213    }
214
215    #[cfg(test)]
216    fn count(&self) -> usize {
217        self.state
218            .lock()
219            .expect("credential event subscribers mutex poisoned")
220            .subscribers
221            .len()
222    }
223}
224
225/// Atomic starting point for an ordered credential-event consumer.
226///
227/// `snapshot` contains only the last status published before `events` was
228/// registered. Every publication after registration is queued in `events`, so
229/// a terminal snapshot can never overtake that generation's queued Pending.
230pub struct CredentialReadEventHandoff {
231    pub snapshot: Option<CredentialReadStatus>,
232    pub events: CredentialReadEventSubscription,
233}
234
235/// One owned subscription to future credential-read lifecycle events.
236///
237/// Dropping this value unregisters it immediately. A subscriber that falls
238/// behind the small bounded queue is also unregistered and observes the stream
239/// close after draining the events already retained in order.
240pub struct CredentialReadEventSubscription {
241    id: u64,
242    receiver: mpsc::Receiver<CredentialReadStatus>,
243    close_updates: watch::Receiver<Option<CredentialReadEventCloseReason>>,
244    subscribers: Weak<CredentialReadEventSubscribers>,
245}
246
247impl CredentialReadEventSubscription {
248    pub async fn recv(&mut self) -> Result<CredentialReadStatus, CredentialReadEventCloseReason> {
249        match self.receiver.recv().await {
250            Some(status) => Ok(status),
251            None => {
252                Err((*self.close_updates.borrow())
253                    .unwrap_or(CredentialReadEventCloseReason::Closed))
254            }
255        }
256    }
257
258    /// Wait independently for this subscription to become unusable.
259    ///
260    /// This wakes even when the event consumer is blocked writing a prior
261    /// status to its downstream transport.
262    pub fn closed(&self) -> impl Future<Output = CredentialReadEventCloseReason> + Send + 'static {
263        let mut updates = self.close_updates.clone();
264        async move {
265            loop {
266                if let Some(reason) = *updates.borrow_and_update() {
267                    return reason;
268                }
269                if updates.changed().await.is_err() {
270                    return CredentialReadEventCloseReason::Closed;
271                }
272            }
273        }
274    }
275}
276
277impl Drop for CredentialReadEventSubscription {
278    fn drop(&mut self) {
279        if let Some(subscribers) = self.subscribers.upgrade() {
280            subscribers.unsubscribe(self.id);
281        }
282    }
283}
284
285type CredentialReadResult = Result<Option<ResolvedParsleeCredential>, CredentialReadError>;
286type ReaderFuture = Pin<Box<dyn Future<Output = CredentialReadResult> + Send + 'static>>;
287
288trait CredentialReader: Clone + Send + Sync + 'static {
289    fn read(&self, purpose: CredentialReadPurpose) -> ReaderFuture;
290}
291
292#[derive(Clone, Copy)]
293struct SystemCredentialReader;
294
295impl CredentialReader for SystemCredentialReader {
296    fn read(&self, purpose: CredentialReadPurpose) -> ReaderFuture {
297        Box::pin(async move { super::resolve_credential_once(purpose).await })
298    }
299}
300
301#[derive(Debug, Clone)]
302enum FlightState {
303    Pending,
304    Terminal(CredentialReadResult),
305}
306
307struct Flight {
308    generation: u64,
309    purpose: CredentialReadPurpose,
310    retry_cutoff: AtomicU64,
311    refresh_cutoff: AtomicU64,
312    updates: watch::Sender<FlightState>,
313}
314
315impl Flight {
316    fn new(
317        generation: u64,
318        purpose: CredentialReadPurpose,
319        retry_cutoff: u64,
320        refresh_cutoff: u64,
321    ) -> Self {
322        let (updates, _initial_receiver) = watch::channel(FlightState::Pending);
323        Self {
324            generation,
325            purpose,
326            retry_cutoff: AtomicU64::new(retry_cutoff),
327            refresh_cutoff: AtomicU64::new(refresh_cutoff),
328            updates,
329        }
330    }
331
332    fn terminal_result(&self) -> Option<CredentialReadResult> {
333        match &*self.updates.borrow() {
334            FlightState::Pending => None,
335            FlightState::Terminal(result) => Some(result.clone()),
336        }
337    }
338
339    fn publish_terminal(&self, result: CredentialReadResult) {
340        self.updates.send_replace(FlightState::Terminal(result));
341    }
342
343    async fn wait(&self) -> CredentialReadResult {
344        // Subscribe before rechecking the current value. If the reader
345        // publishes between these operations, watch retains the terminal value
346        // and changed() cannot be the only wakeup path.
347        let mut updates = self.updates.subscribe();
348        loop {
349            if let FlightState::Terminal(result) = &*updates.borrow_and_update() {
350                return result.clone();
351            }
352            updates
353                .changed()
354                .await
355                .expect("credential flight sender is owned by the flight");
356        }
357    }
358}
359
360struct CoordinatorState {
361    generation: u64,
362    flight: Option<Arc<Flight>>,
363    cooldown: Option<CredentialReadError>,
364}
365
366struct CoordinatorInner<R: CredentialReader> {
367    reader: R,
368    retry_intents: AtomicU64,
369    refresh_intents: AtomicU64,
370    state: Mutex<CoordinatorState>,
371    public_updates: watch::Sender<Option<CredentialReadStatus>>,
372    event_subscribers: Arc<CredentialReadEventSubscribers>,
373}
374
375impl<R: CredentialReader> CoordinatorInner<R> {
376    fn publish_status(&self, status: CredentialReadStatus) {
377        self.public_updates.send_replace(Some(status));
378        self.event_subscribers.publish(status);
379    }
380}
381
382struct CredentialReadCoordinator<R: CredentialReader> {
383    inner: Arc<CoordinatorInner<R>>,
384}
385
386impl<R: CredentialReader> Clone for CredentialReadCoordinator<R> {
387    fn clone(&self) -> Self {
388        Self {
389            inner: Arc::clone(&self.inner),
390        }
391    }
392}
393
394impl<R: CredentialReader> CredentialReadCoordinator<R> {
395    fn new(reader: R) -> Self {
396        let (public_updates, _initial_receiver) = watch::channel(None);
397        Self {
398            inner: Arc::new(CoordinatorInner {
399                reader,
400                retry_intents: AtomicU64::new(0),
401                refresh_intents: AtomicU64::new(0),
402                state: Mutex::new(CoordinatorState {
403                    generation: 0,
404                    flight: None,
405                    cooldown: None,
406                }),
407                public_updates,
408                event_subscribers: Arc::new(CredentialReadEventSubscribers::new()),
409            }),
410        }
411    }
412
413    fn subscribe(&self) -> watch::Receiver<Option<CredentialReadStatus>> {
414        self.inner.public_updates.subscribe()
415    }
416
417    fn subscribe_events(&self) -> CredentialReadEventSubscription {
418        self.inner.event_subscribers.subscribe()
419    }
420
421    fn subscribe_events_with_snapshot(&self) -> CredentialReadEventHandoff {
422        self.inner.event_subscribers.subscribe_with_snapshot()
423    }
424
425    #[cfg(test)]
426    fn event_subscriber_count(&self) -> usize {
427        self.inner.event_subscribers.count()
428    }
429
430    #[cfg(test)]
431    fn event_queue_capacity(&self) -> usize {
432        CREDENTIAL_READ_EVENT_QUEUE_CAPACITY
433    }
434
435    fn resolve(
436        &self,
437        mode: CredentialReadMode,
438    ) -> impl Future<Output = CredentialReadResult> + Send + 'static {
439        let purpose = match mode {
440            CredentialReadMode::Use => CredentialReadPurpose::Resolve,
441            CredentialReadMode::Retry => CredentialReadPurpose::AuthoritativeResolve,
442        };
443        self.resolve_for(mode, purpose)
444    }
445
446    fn refresh(&self) -> impl Future<Output = CredentialReadResult> + Send + 'static {
447        self.resolve_for(CredentialReadMode::Use, CredentialReadPurpose::ForceRefresh)
448    }
449
450    fn refresh_after_rejection(
451        &self,
452        rejected_bearer: super::RejectedBearerKey,
453    ) -> impl Future<Output = CredentialReadResult> + Send + 'static {
454        self.resolve_for(
455            CredentialReadMode::Use,
456            CredentialReadPurpose::AutomaticRefresh(rejected_bearer),
457        )
458    }
459
460    fn resolve_for(
461        &self,
462        mode: CredentialReadMode,
463        purpose: CredentialReadPurpose,
464    ) -> impl Future<Output = CredentialReadResult> + Send + 'static {
465        let coordinator = self.clone();
466        // Allocate Retry intent when the future is created, not when it first
467        // gets polled. A flight records every intent created before terminal
468        // publication, so simultaneous callers still join it even when the
469        // injected/store reader completes before a follower acquires the lock.
470        let retry_intent = coordinator.register_retry_intent(mode);
471        let refresh_intent = coordinator.register_refresh_intent(purpose);
472        async move {
473            loop {
474                let flight = coordinator
475                    .acquire_flight(mode, purpose, retry_intent, refresh_intent)
476                    .await?;
477                let result = flight.wait().await;
478                if flight_result_satisfies(purpose, flight.purpose, &result) {
479                    return result;
480                }
481                // Incompatible purposes may join a pending flight so physical
482                // reads never overlap. Once that flight succeeds, loop to
483                // create/join the requested generation. In particular, a
484                // forced `None` cannot sign out an ordinary waiter, and a
485                // Retry cannot accept a cache-eligible ordinary resolution.
486            }
487        }
488    }
489
490    fn register_retry_intent(&self, mode: CredentialReadMode) -> u64 {
491        match mode {
492            CredentialReadMode::Use => 0,
493            CredentialReadMode::Retry => self
494                .inner
495                .retry_intents
496                .fetch_add(1, AtomicOrdering::SeqCst)
497                .saturating_add(1),
498        }
499    }
500
501    fn register_refresh_intent(&self, purpose: CredentialReadPurpose) -> u64 {
502        match purpose {
503            CredentialReadPurpose::Resolve | CredentialReadPurpose::AuthoritativeResolve => 0,
504            CredentialReadPurpose::ForceRefresh | CredentialReadPurpose::AutomaticRefresh(_) => {
505                self.inner
506                    .refresh_intents
507                    .fetch_add(1, AtomicOrdering::SeqCst)
508                    .saturating_add(1)
509            }
510        }
511    }
512
513    async fn acquire_flight(
514        &self,
515        mode: CredentialReadMode,
516        purpose: CredentialReadPurpose,
517        retry_intent: u64,
518        refresh_intent: u64,
519    ) -> Result<Arc<Flight>, CredentialReadError> {
520        let mut state = self.inner.state.lock().await;
521        if let Some(flight) = state.flight.as_ref() {
522            if flight.terminal_result().is_none() {
523                if mode == CredentialReadMode::Retry {
524                    flight
525                        .retry_cutoff
526                        .fetch_max(retry_intent, AtomicOrdering::SeqCst);
527                }
528                if matches!(
529                    purpose,
530                    CredentialReadPurpose::ForceRefresh
531                        | CredentialReadPurpose::AutomaticRefresh(_)
532                ) {
533                    flight
534                        .refresh_cutoff
535                        .fetch_max(refresh_intent, AtomicOrdering::SeqCst);
536                }
537                return Ok(Arc::clone(flight));
538            }
539            if mode == CredentialReadMode::Retry
540                && retry_intent <= flight.retry_cutoff.load(AtomicOrdering::SeqCst)
541                && flight
542                    .terminal_result()
543                    .as_ref()
544                    .is_some_and(|result| flight_result_satisfies(purpose, flight.purpose, result))
545            {
546                return Ok(Arc::clone(flight));
547            }
548            if matches!(
549                (purpose, flight.purpose),
550                (
551                    CredentialReadPurpose::ForceRefresh,
552                    CredentialReadPurpose::ForceRefresh
553                )
554            ) && refresh_intent <= flight.refresh_cutoff.load(AtomicOrdering::SeqCst)
555            {
556                return Ok(Arc::clone(flight));
557            }
558            if matches!(
559                (purpose, flight.purpose),
560                (
561                    CredentialReadPurpose::AutomaticRefresh(requested),
562                    CredentialReadPurpose::AutomaticRefresh(completed)
563                ) if requested == completed
564            ) && refresh_intent <= flight.refresh_cutoff.load(AtomicOrdering::SeqCst)
565            {
566                return Ok(Arc::clone(flight));
567            }
568        }
569
570        if let Some(failure) = state.cooldown.as_ref() {
571            // Retry and an explicit forced refresh are deliberate caller
572            // actions, so cooldown refuses neither: each clears it and starts
573            // one coalesced read, and a failed read re-arms it below before
574            // any waiter wakes. Ordinary resolution and automatic
575            // rejected-bearer refresh stay refused; that stops prompt storms.
576            if mode == CredentialReadMode::Use && purpose != CredentialReadPurpose::ForceRefresh {
577                return Err(CredentialReadError {
578                    kind: CredentialReadFailureKind::Cooldown,
579                    message: format!(
580                        "credential access is in cooldown after {}; explicitly retry Keychain access",
581                        failure.kind.label()
582                    ),
583                });
584            }
585            state.cooldown = None;
586        }
587
588        state.generation = state.generation.saturating_add(1);
589        let retry_cutoff = self.inner.retry_intents.load(AtomicOrdering::SeqCst);
590        let refresh_cutoff = self.inner.refresh_intents.load(AtomicOrdering::SeqCst);
591        let flight = Arc::new(Flight::new(
592            state.generation,
593            purpose,
594            retry_cutoff,
595            refresh_cutoff,
596        ));
597        state.flight = Some(Arc::clone(&flight));
598        self.inner.publish_status(CredentialReadStatus {
599            generation: flight.generation,
600            state: CredentialReadStatusState::Pending,
601        });
602        drop(state);
603
604        let inner = Arc::clone(&self.inner);
605        let owned_flight = Arc::clone(&flight);
606        tokio::spawn(async move {
607            let result = inner.reader.read(owned_flight.purpose).await;
608            if let Err(error) = &result {
609                // Install cooldown before waking any waiter with the terminal
610                // failure. Otherwise an immediate ordinary Use could race the
611                // background task and start another prompt in the gap.
612                let mut state = inner.state.lock().await;
613                if state
614                    .flight
615                    .as_ref()
616                    .is_some_and(|flight| Arc::ptr_eq(flight, &owned_flight))
617                {
618                    state.cooldown = Some(error.clone());
619                }
620            }
621            // Linearize terminal publication after every Retry future already
622            // created for this generation. Those callers may not have polled
623            // yet; their intent IDs let them join this result without turning
624            // an immediate completion into a second physical read. A later
625            // explicit Retry receives a larger ID and may create a generation.
626            owned_flight.retry_cutoff.fetch_max(
627                inner.retry_intents.load(AtomicOrdering::SeqCst),
628                AtomicOrdering::SeqCst,
629            );
630            owned_flight.refresh_cutoff.fetch_max(
631                inner.refresh_intents.load(AtomicOrdering::SeqCst),
632                AtomicOrdering::SeqCst,
633            );
634            // Queue the public terminal before waking flight waiters. A waiter
635            // may immediately start the next generation on another executor
636            // thread; publishing in this order prevents that generation's
637            // Pending event from overtaking this terminal event.
638            inner.publish_status(CredentialReadStatus {
639                generation: owned_flight.generation,
640                state: public_state(owned_flight.purpose, &result),
641            });
642            // The independently owned flight receives exactly one terminal
643            // publication even when the request that created it is gone.
644            owned_flight.publish_terminal(result);
645        });
646        Ok(flight)
647    }
648
649    #[cfg(test)]
650    async fn resolve_after_terminal_publish_for_test(
651        &self,
652        mode: CredentialReadMode,
653    ) -> CredentialReadResult {
654        let retry_intent = self.register_retry_intent(mode);
655        let flight = self
656            .acquire_flight(mode, CredentialReadPurpose::Resolve, retry_intent, 0)
657            .await?;
658        while flight.terminal_result().is_none() {
659            tokio::task::yield_now().await;
660        }
661        flight.wait().await
662    }
663}
664
665fn flight_result_satisfies(
666    requested: CredentialReadPurpose,
667    completed: CredentialReadPurpose,
668    result: &CredentialReadResult,
669) -> bool {
670    if let CredentialReadPurpose::AutomaticRefresh(requested_bearer) = requested {
671        return matches!(
672            completed,
673            CredentialReadPurpose::AutomaticRefresh(completed_bearer)
674                if requested_bearer == completed_bearer
675        );
676    }
677    if result.is_err() {
678        return true;
679    }
680    match requested {
681        CredentialReadPurpose::Resolve => {
682            !matches!(
683                completed,
684                CredentialReadPurpose::ForceRefresh | CredentialReadPurpose::AutomaticRefresh(_)
685            ) || matches!(result, Ok(Some(_)))
686        }
687        CredentialReadPurpose::AuthoritativeResolve => match completed {
688            CredentialReadPurpose::Resolve => false,
689            CredentialReadPurpose::AuthoritativeResolve => true,
690            CredentialReadPurpose::ForceRefresh | CredentialReadPurpose::AutomaticRefresh(_) => {
691                matches!(result, Ok(Some(_)))
692            }
693        },
694        CredentialReadPurpose::ForceRefresh => completed == CredentialReadPurpose::ForceRefresh,
695        CredentialReadPurpose::AutomaticRefresh(_) => unreachable!("handled above"),
696    }
697}
698
699impl CredentialReadFailureKind {
700    fn label(self) -> &'static str {
701        match self {
702            Self::Denied => "access was denied",
703            Self::Cancelled => "access was cancelled",
704            Self::TimedOut => "the credential helper timed out",
705            Self::Unreadable => "the credential store was unreadable",
706            Self::Cooldown => "a previous credential read failed",
707        }
708    }
709}
710
711fn public_state(
712    purpose: CredentialReadPurpose,
713    result: &CredentialReadResult,
714) -> CredentialReadStatusState {
715    match result {
716        Ok(Some(_)) => CredentialReadStatusState::Configured,
717        // A forced refresh may be unavailable because the configured account
718        // has no refresh token or the token endpoint is offline. `None` tells
719        // the caller not to retry the rejected bearer; it is not evidence that
720        // the credential observed at the start of this generation disappeared.
721        Ok(None)
722            if matches!(
723                purpose,
724                CredentialReadPurpose::ForceRefresh | CredentialReadPurpose::AutomaticRefresh(_)
725            ) =>
726        {
727            CredentialReadStatusState::Configured
728        }
729        Ok(None) => CredentialReadStatusState::SignedOut,
730        Err(error) => match error.kind {
731            CredentialReadFailureKind::Denied => CredentialReadStatusState::Denied,
732            CredentialReadFailureKind::Cancelled => CredentialReadStatusState::Cancelled,
733            CredentialReadFailureKind::TimedOut => CredentialReadStatusState::TimedOut,
734            CredentialReadFailureKind::Unreadable | CredentialReadFailureKind::Cooldown => {
735                CredentialReadStatusState::Unreadable
736            }
737        },
738    }
739}
740
741fn process_coordinator() -> &'static CredentialReadCoordinator<SystemCredentialReader> {
742    static COORDINATOR: OnceLock<CredentialReadCoordinator<SystemCredentialReader>> =
743        OnceLock::new();
744    COORDINATOR.get_or_init(|| CredentialReadCoordinator::new(SystemCredentialReader))
745}
746
747/// Resolve one Parslee credential through the process-owned flight.
748pub async fn resolve_credential(
749    mode: CredentialReadMode,
750) -> Result<Option<ResolvedParsleeCredential>, CredentialReadError> {
751    process_coordinator().resolve(mode).await
752}
753
754/// Force one coordinator-owned Parslee refresh after a server auth rejection.
755pub async fn refresh_credential() -> Result<Option<ResolvedParsleeCredential>, CredentialReadError>
756{
757    process_coordinator().refresh().await
758}
759
760/// Refresh only for the exact bearer rejected by an automatic request.
761///
762/// Intent registration occurs when this function is called, before its future
763/// is polled, so a concurrently completing flight cannot lose a waiting
764/// credential's follow-up generation.
765pub(crate) fn refresh_credential_after_rejection(
766    rejected_bearer: super::RejectedBearerKey,
767) -> impl Future<Output = CredentialReadResult> + Send + 'static {
768    process_coordinator().refresh_after_rejection(rejected_bearer)
769}
770
771/// Subscribe to the latest secret-free credential-read state snapshot.
772pub fn subscribe_credential_read_updates() -> watch::Receiver<Option<CredentialReadStatus>> {
773    process_coordinator().subscribe()
774}
775
776/// Subscribe to future secret-free credential-read events without coalescing.
777///
778/// The stream does not replay the current snapshot. For every generation that
779/// starts after subscription, a consumer that keeps pace receives `Pending`
780/// followed by exactly one terminal state in publication order. A lagging
781/// subscription closes instead of dropping or reordering lifecycle events.
782pub fn subscribe_credential_read_events() -> CredentialReadEventSubscription {
783    process_coordinator().subscribe_events()
784}
785
786/// Atomically capture the last secret-free status and subscribe to later ones.
787///
788/// The snapshot and ordered queue are linearized under one credential-layer
789/// lock. A lifecycle completed before subscription appears only as the
790/// snapshot; a lifecycle started after subscription appears in exact
791/// `Pending`-then-terminal queue order.
792pub fn subscribe_credential_read_event_handoff() -> CredentialReadEventHandoff {
793    process_coordinator().subscribe_events_with_snapshot()
794}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799    use std::collections::VecDeque;
800    use std::sync::atomic::{AtomicUsize, Ordering};
801    use std::sync::{Arc, Mutex};
802    use tokio::sync::Notify;
803
804    #[derive(Clone)]
805    struct CountingReader {
806        inner: Arc<CountingReaderInner>,
807    }
808
809    struct CountingReaderInner {
810        calls: AtomicUsize,
811        outcomes: Mutex<VecDeque<CredentialReadResult>>,
812        blocked: bool,
813        release: Notify,
814    }
815
816    impl CountingReader {
817        fn blocked() -> Self {
818            Self {
819                inner: Arc::new(CountingReaderInner {
820                    calls: AtomicUsize::new(0),
821                    outcomes: Mutex::new(VecDeque::new()),
822                    blocked: true,
823                    release: Notify::new(),
824                }),
825            }
826        }
827
828        fn sequence(outcomes: impl IntoIterator<Item = CredentialReadResult>) -> Self {
829            Self {
830                inner: Arc::new(CountingReaderInner {
831                    calls: AtomicUsize::new(0),
832                    outcomes: Mutex::new(outcomes.into_iter().collect()),
833                    blocked: false,
834                    release: Notify::new(),
835                }),
836            }
837        }
838
839        fn immediate_success(credential: ResolvedParsleeCredential) -> Self {
840            Self::sequence([Ok(Some(credential))])
841        }
842
843        fn release_success(&self, credential: ResolvedParsleeCredential) {
844            self.release(Ok(Some(credential)));
845        }
846
847        fn release(&self, outcome: CredentialReadResult) {
848            self.inner.outcomes.lock().unwrap().push_back(outcome);
849            // Retain a permit if the independently owned reader has not
850            // reached `notified()` yet; the cancellation test must not depend
851            // on scheduler timing.
852            self.inner.release.notify_one();
853        }
854
855        fn calls(&self) -> usize {
856            self.inner.calls.load(Ordering::SeqCst)
857        }
858    }
859
860    #[derive(Clone)]
861    struct CachedThenPhysicalReader {
862        cached: ResolvedParsleeCredential,
863        physical_calls: Arc<AtomicUsize>,
864        outcomes: Arc<Mutex<VecDeque<CredentialReadResult>>>,
865    }
866
867    impl CachedThenPhysicalReader {
868        fn new(
869            cached: ResolvedParsleeCredential,
870            outcomes: impl IntoIterator<Item = CredentialReadResult>,
871        ) -> Self {
872            Self {
873                cached,
874                physical_calls: Arc::new(AtomicUsize::new(0)),
875                outcomes: Arc::new(Mutex::new(outcomes.into_iter().collect())),
876            }
877        }
878
879        fn physical_calls(&self) -> usize {
880            self.physical_calls.load(Ordering::SeqCst)
881        }
882    }
883
884    impl CredentialReader for CachedThenPhysicalReader {
885        fn read(&self, purpose: CredentialReadPurpose) -> ReaderFuture {
886            let reader = self.clone();
887            Box::pin(async move {
888                if purpose == CredentialReadPurpose::Resolve {
889                    return Ok(Some(reader.cached));
890                }
891                reader.physical_calls.fetch_add(1, Ordering::SeqCst);
892                reader
893                    .outcomes
894                    .lock()
895                    .unwrap()
896                    .pop_front()
897                    .expect("injected physical credential reader exhausted")
898            })
899        }
900    }
901
902    impl CredentialReader for CountingReader {
903        fn read(&self, _purpose: CredentialReadPurpose) -> ReaderFuture {
904            let reader = self.clone();
905            Box::pin(async move {
906                reader.inner.calls.fetch_add(1, Ordering::SeqCst);
907                if reader.inner.blocked {
908                    loop {
909                        if let Some(outcome) = reader.inner.outcomes.lock().unwrap().pop_front() {
910                            return outcome;
911                        }
912                        reader.inner.release.notified().await;
913                    }
914                }
915                reader
916                    .inner
917                    .outcomes
918                    .lock()
919                    .unwrap()
920                    .pop_front()
921                    .expect("injected credential reader exhausted")
922            })
923        }
924    }
925
926    fn fixture_credential() -> ResolvedParsleeCredential {
927        ResolvedParsleeCredential {
928            access_token: "fixture-access-token".into(),
929            api_base: "https://fixture.parslee.test".into(),
930            expires_at: 1_800_000_000,
931        }
932    }
933
934    fn denied() -> CredentialReadResult {
935        Err(CredentialReadError {
936            kind: CredentialReadFailureKind::Denied,
937            message: "credential access denied".into(),
938        })
939    }
940
941    fn success() -> CredentialReadResult {
942        Ok(Some(fixture_credential()))
943    }
944
945    fn replacement_credential() -> ResolvedParsleeCredential {
946        ResolvedParsleeCredential {
947            access_token: "replacement-access-token".into(),
948            api_base: "https://replacement.parslee.test".into(),
949            expires_at: 1_900_000_000,
950        }
951    }
952
953    #[tokio::test]
954    async fn concurrent_reads_share_one_cancellation_safe_flight() {
955        let reader = CountingReader::blocked();
956        let coordinator = CredentialReadCoordinator::new(reader.clone());
957        let mut public_updates = coordinator.subscribe();
958        let first = tokio::spawn(coordinator.clone().resolve(CredentialReadMode::Use));
959        let second = tokio::spawn(coordinator.clone().resolve(CredentialReadMode::Use));
960
961        public_updates.changed().await.unwrap();
962        assert_eq!(
963            *public_updates.borrow_and_update(),
964            Some(CredentialReadStatus {
965                generation: 1,
966                state: CredentialReadStatusState::Pending,
967            })
968        );
969        first.abort();
970        reader.release_success(fixture_credential());
971
972        assert_eq!(second.await.unwrap(), Ok(Some(fixture_credential())));
973        public_updates.changed().await.unwrap();
974        assert_eq!(
975            *public_updates.borrow_and_update(),
976            Some(CredentialReadStatus {
977                generation: 1,
978                state: CredentialReadStatusState::Configured,
979            })
980        );
981        assert_eq!(reader.calls(), 1);
982    }
983
984    #[tokio::test]
985    async fn denial_requires_explicit_retry() {
986        let reader = CountingReader::sequence([denied(), success()]);
987        let coordinator = CredentialReadCoordinator::new(reader.clone());
988
989        assert_eq!(
990            coordinator
991                .resolve(CredentialReadMode::Use)
992                .await
993                .unwrap_err()
994                .kind,
995            CredentialReadFailureKind::Denied
996        );
997        assert_eq!(
998            coordinator
999                .resolve(CredentialReadMode::Use)
1000                .await
1001                .unwrap_err()
1002                .kind,
1003            CredentialReadFailureKind::Cooldown
1004        );
1005        assert_eq!(
1006            coordinator.resolve(CredentialReadMode::Retry).await,
1007            success()
1008        );
1009        assert_eq!(reader.calls(), 2);
1010    }
1011
1012    #[tokio::test]
1013    async fn terminal_publish_before_waiter_subscription_cannot_lose_wakeup() {
1014        let reader = CountingReader::immediate_success(fixture_credential());
1015        let coordinator = CredentialReadCoordinator::new(reader.clone());
1016
1017        let result = coordinator
1018            .resolve_after_terminal_publish_for_test(CredentialReadMode::Use)
1019            .await;
1020
1021        assert_eq!(result, success());
1022        assert_eq!(reader.calls(), 1);
1023    }
1024
1025    #[tokio::test]
1026    async fn immediately_completed_read_preserves_pending_before_terminal_for_event_consumers() {
1027        let reader = CountingReader::immediate_success(fixture_credential());
1028        let coordinator = CredentialReadCoordinator::new(reader.clone());
1029        let mut events = coordinator.subscribe_events();
1030
1031        assert_eq!(
1032            coordinator.resolve(CredentialReadMode::Use).await,
1033            success()
1034        );
1035
1036        assert_eq!(
1037            events.recv().await,
1038            Ok(CredentialReadStatus {
1039                generation: 1,
1040                state: CredentialReadStatusState::Pending,
1041            }),
1042            "a stalled event consumer must still observe pending before terminal"
1043        );
1044        assert_eq!(
1045            events.recv().await,
1046            Ok(CredentialReadStatus {
1047                generation: 1,
1048                state: CredentialReadStatusState::Configured,
1049            })
1050        );
1051        assert!(
1052            tokio::time::timeout(std::time::Duration::from_millis(25), events.recv())
1053                .await
1054                .is_err(),
1055            "one generation must publish exactly one terminal event"
1056        );
1057        assert_eq!(reader.calls(), 1);
1058    }
1059
1060    #[tokio::test]
1061    async fn atomic_handoff_queues_complete_lifecycle_started_after_subscription() {
1062        let reader = CountingReader::immediate_success(fixture_credential());
1063        let coordinator = CredentialReadCoordinator::new(reader);
1064        let CredentialReadEventHandoff {
1065            snapshot,
1066            mut events,
1067        } = coordinator.subscribe_events_with_snapshot();
1068
1069        assert_eq!(snapshot, None);
1070        assert_eq!(
1071            coordinator.resolve(CredentialReadMode::Use).await,
1072            success()
1073        );
1074        assert_eq!(
1075            events.recv().await,
1076            Ok(CredentialReadStatus {
1077                generation: 1,
1078                state: CredentialReadStatusState::Pending,
1079            })
1080        );
1081        assert_eq!(
1082            events.recv().await,
1083            Ok(CredentialReadStatus {
1084                generation: 1,
1085                state: CredentialReadStatusState::Configured,
1086            })
1087        );
1088    }
1089
1090    #[tokio::test]
1091    async fn atomic_handoff_reconciles_preexisting_terminal_without_inventing_pending() {
1092        let reader = CountingReader::immediate_success(fixture_credential());
1093        let coordinator = CredentialReadCoordinator::new(reader);
1094        assert_eq!(
1095            coordinator.resolve(CredentialReadMode::Use).await,
1096            success()
1097        );
1098
1099        let CredentialReadEventHandoff {
1100            snapshot,
1101            mut events,
1102        } = coordinator.subscribe_events_with_snapshot();
1103        assert_eq!(
1104            snapshot,
1105            Some(CredentialReadStatus {
1106                generation: 1,
1107                state: CredentialReadStatusState::Configured,
1108            })
1109        );
1110        assert!(
1111            tokio::time::timeout(std::time::Duration::from_millis(25), events.recv())
1112                .await
1113                .is_err(),
1114            "pre-subscription lifecycle must not be replayed into the future queue"
1115        );
1116    }
1117
1118    #[test]
1119    fn dropped_event_subscriptions_unregister_without_waiting_for_publication() {
1120        let coordinator = CredentialReadCoordinator::new(CountingReader::sequence([]));
1121        let baseline = coordinator.event_subscriber_count();
1122
1123        for _ in 0..64 {
1124            let subscription = coordinator.subscribe_events();
1125            assert_eq!(coordinator.event_subscriber_count(), baseline + 1);
1126            drop(subscription);
1127        }
1128
1129        assert_eq!(
1130            coordinator.event_subscriber_count(),
1131            baseline,
1132            "dropping receivers must promptly unregister their global senders"
1133        );
1134    }
1135
1136    #[tokio::test]
1137    async fn stalled_event_subscription_is_closed_at_bounded_capacity() {
1138        let coordinator =
1139            CredentialReadCoordinator::new(CountingReader::sequence((0..3).map(|_| success())));
1140        let baseline = coordinator.event_subscriber_count();
1141        let capacity = coordinator.event_queue_capacity();
1142        let mut subscription = coordinator.subscribe_events();
1143
1144        for generation in 0..3 {
1145            let mode = if generation == 0 {
1146                CredentialReadMode::Use
1147            } else {
1148                CredentialReadMode::Retry
1149            };
1150            assert_eq!(coordinator.resolve(mode).await, success());
1151        }
1152
1153        assert_eq!(
1154            coordinator.event_subscriber_count(),
1155            baseline,
1156            "overflow must remove the lagging subscriber immediately"
1157        );
1158        let mut retained = Vec::new();
1159        for _ in 0..capacity {
1160            retained.push(subscription.recv().await.unwrap());
1161        }
1162        assert_eq!(retained.len(), capacity);
1163        assert_eq!(
1164            retained,
1165            vec![
1166                CredentialReadStatus {
1167                    generation: 1,
1168                    state: CredentialReadStatusState::Pending,
1169                },
1170                CredentialReadStatus {
1171                    generation: 1,
1172                    state: CredentialReadStatusState::Configured,
1173                },
1174                CredentialReadStatus {
1175                    generation: 2,
1176                    state: CredentialReadStatusState::Pending,
1177                },
1178                CredentialReadStatus {
1179                    generation: 2,
1180                    state: CredentialReadStatusState::Configured,
1181                },
1182            ],
1183            "bounded retention must preserve complete lifecycle ordering"
1184        );
1185        assert_eq!(
1186            subscription.recv().await,
1187            Err(CredentialReadEventCloseReason::Lagged)
1188        );
1189        assert_eq!(
1190            subscription.closed().await,
1191            CredentialReadEventCloseReason::Lagged
1192        );
1193    }
1194
1195    #[tokio::test]
1196    async fn simultaneous_explicit_retries_create_one_new_flight() {
1197        let reader = CountingReader::sequence([denied(), success()]);
1198        let coordinator = CredentialReadCoordinator::new(reader.clone());
1199        assert_eq!(
1200            coordinator
1201                .resolve(CredentialReadMode::Use)
1202                .await
1203                .unwrap_err()
1204                .kind,
1205            CredentialReadFailureKind::Denied
1206        );
1207
1208        let (first, second) = tokio::join!(
1209            coordinator.resolve(CredentialReadMode::Retry),
1210            coordinator.resolve(CredentialReadMode::Retry),
1211        );
1212
1213        assert_eq!(first, success());
1214        assert_eq!(second, success());
1215        assert_eq!(reader.calls(), 2);
1216    }
1217
1218    #[tokio::test]
1219    async fn failed_retry_can_be_explicitly_retried_again() {
1220        let reader = CountingReader::sequence([denied(), denied(), success()]);
1221        let coordinator = CredentialReadCoordinator::new(reader.clone());
1222
1223        assert_eq!(
1224            coordinator
1225                .resolve(CredentialReadMode::Use)
1226                .await
1227                .unwrap_err()
1228                .kind,
1229            CredentialReadFailureKind::Denied
1230        );
1231        assert_eq!(
1232            coordinator
1233                .resolve(CredentialReadMode::Retry)
1234                .await
1235                .unwrap_err()
1236                .kind,
1237            CredentialReadFailureKind::Denied
1238        );
1239        assert_eq!(
1240            coordinator
1241                .resolve(CredentialReadMode::Use)
1242                .await
1243                .unwrap_err()
1244                .kind,
1245            CredentialReadFailureKind::Cooldown
1246        );
1247        assert_eq!(
1248            coordinator.resolve(CredentialReadMode::Retry).await,
1249            success()
1250        );
1251        assert_eq!(reader.calls(), 3);
1252    }
1253
1254    #[tokio::test]
1255    async fn failed_reactive_refresh_installs_cooldown_for_later_use() {
1256        let reader = CountingReader::sequence([denied(), success()]);
1257        let coordinator = CredentialReadCoordinator::new(reader.clone());
1258
1259        assert_eq!(
1260            coordinator.refresh().await.unwrap_err().kind,
1261            CredentialReadFailureKind::Denied
1262        );
1263        assert_eq!(
1264            coordinator
1265                .resolve(CredentialReadMode::Use)
1266                .await
1267                .unwrap_err()
1268                .kind,
1269            CredentialReadFailureKind::Cooldown
1270        );
1271        assert_eq!(reader.calls(), 1);
1272    }
1273
1274    fn refresh_commit_failure() -> CredentialReadResult {
1275        // The shape the store reader returns when a forced refresh's token
1276        // grant succeeds but committing the refreshed credential fails.
1277        Err(CredentialReadError {
1278            kind: CredentialReadFailureKind::Unreadable,
1279            message: "reactive Parslee refresh commit failed: injected publish failure".into(),
1280        })
1281    }
1282
1283    #[tokio::test]
1284    async fn forced_refresh_after_failed_forced_refresh_commit_reaches_reader() {
1285        let reader = CountingReader::sequence([refresh_commit_failure(), success()]);
1286        let coordinator = CredentialReadCoordinator::new(reader.clone());
1287
1288        assert_eq!(
1289            coordinator.refresh().await.unwrap_err().kind,
1290            CredentialReadFailureKind::Unreadable
1291        );
1292        assert_eq!(
1293            coordinator
1294                .resolve(CredentialReadMode::Use)
1295                .await
1296                .unwrap_err()
1297                .kind,
1298            CredentialReadFailureKind::Cooldown,
1299            "an ordinary Use stays in cooldown after a failed forced refresh"
1300        );
1301        assert_eq!(reader.calls(), 1);
1302
1303        assert_eq!(
1304            coordinator.refresh().await,
1305            success(),
1306            "a later explicit ForceRefresh must not be blocked by a prior commit failure"
1307        );
1308        assert_eq!(
1309            reader.calls(),
1310            2,
1311            "the second forced refresh must reach the reader, not short-circuit on Cooldown"
1312        );
1313    }
1314
1315    #[tokio::test]
1316    async fn ordinary_use_stays_in_cooldown_when_bypassing_forced_refresh_fails() {
1317        let reader = CountingReader::sequence([denied(), denied()]);
1318        let coordinator = CredentialReadCoordinator::new(reader.clone());
1319        let rejected = super::super::rejected_bearer_key("fixture-access-token");
1320
1321        assert_eq!(
1322            coordinator
1323                .resolve(CredentialReadMode::Use)
1324                .await
1325                .unwrap_err()
1326                .kind,
1327            CredentialReadFailureKind::Denied
1328        );
1329        assert_eq!(
1330            coordinator
1331                .resolve(CredentialReadMode::Use)
1332                .await
1333                .unwrap_err()
1334                .kind,
1335            CredentialReadFailureKind::Cooldown
1336        );
1337        assert_eq!(reader.calls(), 1);
1338
1339        assert_eq!(
1340            coordinator.refresh().await.unwrap_err().kind,
1341            CredentialReadFailureKind::Denied,
1342            "an explicit forced refresh makes one new read instead of returning Cooldown"
1343        );
1344        assert_eq!(reader.calls(), 2);
1345        assert_eq!(
1346            coordinator
1347                .resolve(CredentialReadMode::Use)
1348                .await
1349                .unwrap_err()
1350                .kind,
1351            CredentialReadFailureKind::Cooldown,
1352            "the forced refresh's own failure re-arms cooldown for ordinary Use"
1353        );
1354        assert_eq!(
1355            coordinator
1356                .refresh_after_rejection(rejected)
1357                .await
1358                .unwrap_err()
1359                .kind,
1360            CredentialReadFailureKind::Cooldown,
1361            "automatic rejected-bearer recovery does not bypass cooldown"
1362        );
1363        assert_eq!(
1364            reader.calls(),
1365            2,
1366            "neither cooled-down caller may start a read"
1367        );
1368    }
1369
1370    #[tokio::test]
1371    async fn forced_refresh_after_keychain_denial_reaches_reader_and_success_clears_cooldown() {
1372        let reader = CountingReader::sequence([denied(), success(), success()]);
1373        let coordinator = CredentialReadCoordinator::new(reader.clone());
1374
1375        assert_eq!(
1376            coordinator
1377                .resolve(CredentialReadMode::Use)
1378                .await
1379                .unwrap_err()
1380                .kind,
1381            CredentialReadFailureKind::Denied
1382        );
1383        assert_eq!(
1384            coordinator.refresh().await,
1385            success(),
1386            "an explicit forced refresh after a Keychain denial makes one new read"
1387        );
1388        assert_eq!(reader.calls(), 2);
1389        assert_eq!(
1390            coordinator.resolve(CredentialReadMode::Use).await,
1391            success(),
1392            "a successful forced read clears the cooldown the same way Retry does"
1393        );
1394        assert_eq!(reader.calls(), 3);
1395    }
1396
1397    #[tokio::test]
1398    async fn simultaneous_reactive_refreshes_share_one_forced_flight() {
1399        let reader = CountingReader::sequence([success()]);
1400        let coordinator = CredentialReadCoordinator::new(reader.clone());
1401
1402        let (first, second) = tokio::join!(coordinator.refresh(), coordinator.refresh());
1403
1404        assert_eq!(first, success());
1405        assert_eq!(second, success());
1406        assert_eq!(reader.calls(), 1);
1407    }
1408
1409    #[tokio::test]
1410    async fn ordinary_resolution_does_not_inherit_none_from_forced_refresh() {
1411        let reader = CountingReader::blocked();
1412        let coordinator = CredentialReadCoordinator::new(reader.clone());
1413
1414        let release = async {
1415            while reader.calls() == 0 {
1416                tokio::task::yield_now().await;
1417            }
1418            reader.release(Ok(None));
1419            tokio::task::yield_now().await;
1420            reader.release_success(fixture_credential());
1421        };
1422        let (refreshed, resolved, ()) = tokio::join!(
1423            coordinator.refresh(),
1424            coordinator.resolve(CredentialReadMode::Use),
1425            release,
1426        );
1427
1428        assert_eq!(refreshed, Ok(None));
1429        assert_eq!(resolved, success());
1430        assert_eq!(reader.calls(), 2);
1431    }
1432
1433    #[tokio::test]
1434    async fn explicit_retries_after_failed_force_bypass_cached_resolution_and_coalesce() {
1435        let replacement = replacement_credential();
1436        let reader = CachedThenPhysicalReader::new(
1437            fixture_credential(),
1438            [denied(), Ok(Some(replacement.clone()))],
1439        );
1440        let coordinator = CredentialReadCoordinator::new(reader.clone());
1441
1442        assert_eq!(
1443            coordinator.resolve(CredentialReadMode::Use).await,
1444            success()
1445        );
1446        assert_eq!(reader.physical_calls(), 0);
1447        assert_eq!(
1448            coordinator.refresh().await.unwrap_err().kind,
1449            CredentialReadFailureKind::Denied
1450        );
1451
1452        let (first, second) = tokio::join!(
1453            coordinator.resolve(CredentialReadMode::Retry),
1454            coordinator.resolve(CredentialReadMode::Retry),
1455        );
1456
1457        assert_eq!(first, Ok(Some(replacement.clone())));
1458        assert_eq!(second, Ok(Some(replacement)));
1459        assert_eq!(reader.physical_calls(), 2);
1460    }
1461
1462    #[tokio::test]
1463    async fn common_v2_resolution_flight_performs_one_physical_get() {
1464        const CHILD_MARKER: &str = "CAR_AUTH_ONE_GET_CHILD";
1465        if std::env::var(CHILD_MARKER).as_deref() != Ok("1") {
1466            let status = std::process::Command::new(std::env::current_exe().unwrap())
1467                .args([
1468                    "--exact",
1469                    "credential_read::tests::common_v2_resolution_flight_performs_one_physical_get",
1470                    "--nocapture",
1471                    "--test-threads=1",
1472                ])
1473                .env(CHILD_MARKER, "1")
1474                .status()
1475                .unwrap();
1476            assert!(status.success(), "isolated one-get assertion failed");
1477            return;
1478        }
1479
1480        let directory = tempfile::tempdir().unwrap();
1481        std::env::set_var("CAR_SECRETS_FILE_DIR", directory.path());
1482        std::env::remove_var(super::super::PARSLEE_ACCESS_TOKEN_KEY);
1483        std::env::remove_var(super::super::PARSLEE_API_BASE_KEY);
1484        super::super::invalidate_access_token_cache();
1485        car_secrets::SecretStore::new()
1486            .publish(
1487                &car_secrets::SecretRef::with_default_service(
1488                    car_secrets::PARSLEE_AUTH_STATE_V2_KEY,
1489                ),
1490                &serde_json::json!({
1491                    "schema": 2,
1492                    "revision": 7,
1493                    "generation": 3,
1494                    "active": {
1495                        "account_id": "one-get-account",
1496                        "access_token": "one-get-access",
1497                        "expires_at": 9_999_999_999_u64,
1498                        "api_base": "https://one-get.example"
1499                    },
1500                    "accounts": [{
1501                        "account_id": "one-get-account",
1502                        "access_token": "one-get-access",
1503                        "expires_at": 9_999_999_999_u64,
1504                        "api_base": "https://one-get.example"
1505                    }]
1506                })
1507                .to_string(),
1508            )
1509            .unwrap();
1510
1511        let before = car_secrets::secret_store_activity();
1512        let resolved = CredentialReadCoordinator::new(SystemCredentialReader)
1513            .resolve(CredentialReadMode::Use)
1514            .await
1515            .unwrap()
1516            .unwrap();
1517        let after = car_secrets::secret_store_activity();
1518
1519        assert_eq!(resolved.api_base, "https://one-get.example");
1520        assert_eq!(after.get_attempts - before.get_attempts, 1);
1521    }
1522}