Skip to main content

car_server_core/
openrouter_auth.rs

1//! Daemon-owned OpenRouter OAuth PKCE flow.
2//!
3//! Hosts open the returned browser URL and poll `openrouter.status`; the daemon
4//! owns the verifier, loopback callback, exchange, timeout, and keychain write.
5
6use serde::Serialize;
7use std::sync::{Arc, Mutex, OnceLock};
8use std::time::{Duration, SystemTime, UNIX_EPOCH};
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::net::TcpListener;
11use tokio::sync::watch;
12use tokio_util::sync::CancellationToken;
13
14const DEFAULT_AUTH_URL: &str = "https://openrouter.ai/auth";
15const DEFAULT_EXCHANGE_URL: &str = "https://openrouter.ai/api/v1/auth/keys";
16pub const TEST_AUTHORIZATION_BASE_URL_ENV: &str = "OPENROUTER_AUTHORIZATION_BASE_URL";
17pub const TEST_EXCHANGE_URL_ENV: &str = "OPENROUTER_EXCHANGE_URL";
18const DEFAULT_TIMEOUT_SECS: u64 = 300;
19const MAX_TIMEOUT_SECS: u64 = 900;
20
21#[derive(Debug, Clone, Serialize)]
22pub struct PendingStatus {
23    pub flow_id: String,
24    pub deadline_unix_ms: u64,
25}
26
27#[derive(Debug, Clone, Serialize)]
28pub struct TerminalResult {
29    pub flow_id: String,
30    pub kind: String,
31    pub message: String,
32}
33
34#[derive(Debug, Clone, Serialize)]
35pub struct Status {
36    pub authority_generation: u64,
37    pub state: String,
38    pub effective_source: String,
39    pub pasted_key_exists: bool,
40    pub oauth_key_exists: bool,
41    pub pending: Option<PendingStatus>,
42    pub last_result: Option<TerminalResult>,
43}
44
45#[derive(Debug, Clone, Serialize)]
46pub struct StartResult {
47    pub authority_generation: u64,
48    pub authorize_url: String,
49    pub flow_id: String,
50    pub deadline_unix_ms: u64,
51}
52
53struct PendingFlow {
54    flow_id: String,
55    action_generation: u64,
56    deadline_unix_ms: u64,
57    cancel: CancellationToken,
58}
59
60#[derive(Clone, Copy, Default)]
61struct CredentialPresence {
62    environment: bool,
63    pasted: bool,
64    oauth: bool,
65}
66
67struct Manager {
68    action_generation: u64,
69    pending: Option<PendingFlow>,
70    last_result: Option<TerminalResult>,
71    credentials: CredentialPresence,
72    credential_revision: u64,
73}
74
75impl Default for Manager {
76    fn default() -> Self {
77        // Seed from wall-clock nanoseconds so a daemon restart does not reset
78        // connected hosts to generation zero. Mutations remain +1 under the
79        // manager mutex for strict in-process ordering.
80        let action_generation = SystemTime::now()
81            .duration_since(UNIX_EPOCH)
82            .unwrap_or_default()
83            .as_nanos()
84            .try_into()
85            .unwrap_or(u64::MAX / 2);
86        Self {
87            action_generation,
88            pending: None,
89            last_result: None,
90            // This one-time source read happens before the manager can reserve
91            // any authority generation. Thereafter the manager mirrors every
92            // daemon-owned credential mutation, keeping generation snapshots
93            // IO-free and bounded by the private vault slots.
94            credentials: credential_presence_from_vault(),
95            credential_revision: 0,
96        }
97    }
98}
99
100impl Manager {
101    fn advance_action_generation(&mut self) -> u64 {
102        self.action_generation = self
103            .action_generation
104            .checked_add(1)
105            .expect("OpenRouter authority generation exhausted");
106        self.action_generation
107    }
108
109    fn set_pasted_exists(&mut self, exists: bool) {
110        if self.credentials.pasted != exists {
111            self.credentials.pasted = exists;
112            self.credential_revision = self.credential_revision.wrapping_add(1);
113        }
114    }
115
116    fn set_oauth_exists(&mut self, exists: bool) {
117        if self.credentials.oauth != exists {
118            self.credentials.oauth = exists;
119            self.credential_revision = self.credential_revision.wrapping_add(1);
120        }
121    }
122
123    fn replace_credential_presence(&mut self, presence: CredentialPresence) {
124        if self.credentials.environment != presence.environment
125            || self.credentials.pasted != presence.pasted
126            || self.credentials.oauth != presence.oauth
127        {
128            self.credentials = presence;
129            self.credential_revision = self.credential_revision.wrapping_add(1);
130        }
131    }
132}
133
134/// Test-only scheduling seam around the manager transaction. In production
135/// this is zero-sized and compiles to a no-op; tests use barriers to prove the
136/// lock, rather than task timing, orders concurrent starts and snapshots.
137#[derive(Clone, Default)]
138struct ManagerTransactionHook {
139    #[cfg(test)]
140    before_lock: Option<Arc<std::sync::Barrier>>,
141    #[cfg(test)]
142    inside_lock_entered: Option<Arc<std::sync::Barrier>>,
143    #[cfg(test)]
144    inside_lock_release: Option<Arc<std::sync::Barrier>>,
145    #[cfg(test)]
146    after_reserve_entered: Option<Arc<std::sync::Barrier>>,
147    #[cfg(test)]
148    after_reserve_release: Option<Arc<std::sync::Barrier>>,
149    #[cfg(test)]
150    before_return_entered: Option<Arc<std::sync::Barrier>>,
151    #[cfg(test)]
152    before_return_release: Option<Arc<std::sync::Barrier>>,
153}
154
155impl ManagerTransactionHook {
156    fn before_lock(&self) {
157        #[cfg(test)]
158        if let Some(barrier) = &self.before_lock {
159            barrier.wait();
160        }
161    }
162
163    fn inside_lock(&self) {
164        #[cfg(test)]
165        {
166            if let Some(barrier) = &self.inside_lock_entered {
167                barrier.wait();
168            }
169            if let Some(barrier) = &self.inside_lock_release {
170                barrier.wait();
171            }
172        }
173    }
174
175    fn after_reserve(&self) {
176        #[cfg(test)]
177        {
178            if let Some(barrier) = &self.after_reserve_entered {
179                barrier.wait();
180            }
181            if let Some(barrier) = &self.after_reserve_release {
182                barrier.wait();
183            }
184        }
185    }
186
187    fn before_return(&self) {
188        #[cfg(test)]
189        {
190            if let Some(barrier) = &self.before_return_entered {
191                barrier.wait();
192            }
193            if let Some(barrier) = &self.before_return_release {
194                barrier.wait();
195            }
196        }
197    }
198}
199
200/// Test-only scheduling seam around disconnect's blocking keychain deletion.
201/// Production uses the zero-sized default. Tests pause after generation
202/// reservation/publish but before deletion to prove clients see supersession
203/// before the vault can block.
204#[derive(Clone, Default)]
205struct DisconnectHook {
206    #[cfg(test)]
207    before_delete_entered: Option<Arc<std::sync::Barrier>>,
208    #[cfg(test)]
209    before_delete_release: Option<Arc<std::sync::Barrier>>,
210    #[cfg(test)]
211    forced_delete_error: Option<String>,
212}
213
214impl DisconnectHook {
215    fn before_delete(&self) {
216        #[cfg(test)]
217        {
218            if let Some(barrier) = &self.before_delete_entered {
219                barrier.wait();
220            }
221            if let Some(barrier) = &self.before_delete_release {
222                barrier.wait();
223            }
224        }
225    }
226
227    fn delete_oauth_key(&self) -> Result<(), String> {
228        self.before_delete();
229        #[cfg(test)]
230        if let Some(error) = &self.forced_delete_error {
231            return Err(error.clone());
232        }
233        delete_oauth_key()
234    }
235}
236
237#[derive(Clone, Default)]
238struct CredentialFinishHook {
239    #[cfg(test)]
240    before_gate_entered: Option<Arc<std::sync::Barrier>>,
241    #[cfg(test)]
242    after_gate_entered: Option<Arc<std::sync::Barrier>>,
243    #[cfg(test)]
244    after_gate_release: Option<Arc<std::sync::Barrier>>,
245}
246
247impl CredentialFinishHook {
248    fn before_gate(&self) {
249        #[cfg(test)]
250        if let Some(barrier) = &self.before_gate_entered {
251            barrier.wait();
252        }
253    }
254
255    fn after_gate(&self) {
256        #[cfg(test)]
257        {
258            if let Some(barrier) = &self.after_gate_entered {
259                barrier.wait();
260            }
261            if let Some(barrier) = &self.after_gate_release {
262                barrier.wait();
263            }
264        }
265    }
266}
267
268#[derive(Clone, Default)]
269struct PastedMutationHook {
270    #[cfg(test)]
271    before_gate_entered: Option<Arc<std::sync::Barrier>>,
272}
273
274impl PastedMutationHook {
275    fn before_gate(&self) {
276        #[cfg(test)]
277        if let Some(barrier) = &self.before_gate_entered {
278            barrier.wait();
279        }
280    }
281}
282
283fn manager() -> &'static Arc<Mutex<Manager>> {
284    static MANAGER: OnceLock<Arc<Mutex<Manager>>> = OnceLock::new();
285    MANAGER.get_or_init(|| Arc::new(Mutex::new(Manager::default())))
286}
287
288fn credential_io_gate() -> &'static tokio::sync::Mutex<()> {
289    static GATE: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
290    GATE.get_or_init(|| tokio::sync::Mutex::new(()))
291}
292
293fn authority_updates() -> &'static watch::Sender<Option<Status>> {
294    static UPDATES: OnceLock<watch::Sender<Option<Status>>> = OnceLock::new();
295    UPDATES.get_or_init(|| watch::channel(None).0)
296}
297
298/// Subscribe to daemon-wide, non-secret OpenRouter authority snapshots.
299/// Every generation-changing mutation publishes before any slower setup or
300/// vault work, so all connected hosts can reject delayed lower-generation
301/// replies even when those replies belong to another WebSocket session.
302pub fn subscribe_authority_updates() -> watch::Receiver<Option<Status>> {
303    authority_updates().subscribe()
304}
305
306fn publish_status(status: &Status) {
307    authority_updates().send_replace(Some(status.clone()));
308}
309
310fn now_unix_ms() -> u64 {
311    SystemTime::now()
312        .duration_since(UNIX_EPOCH)
313        .unwrap_or_default()
314        .as_millis() as u64
315}
316
317fn terminal(flow_id: &str, kind: &str, message: &str) -> TerminalResult {
318    TerminalResult {
319        flow_id: flow_id.to_string(),
320        kind: kind.to_string(),
321        message: message.to_string(),
322    }
323}
324
325#[cfg(not(test))]
326fn oauth_key_exists() -> bool {
327    car_inference::openrouter::oauth_key_exists()
328}
329
330#[cfg(test)]
331fn test_oauth_slot() -> &'static Mutex<Option<String>> {
332    static SLOT: OnceLock<Mutex<Option<String>>> = OnceLock::new();
333    SLOT.get_or_init(|| Mutex::new(None))
334}
335
336#[cfg(test)]
337fn oauth_key_exists() -> bool {
338    test_oauth_slot()
339        .lock()
340        .unwrap_or_else(|p| p.into_inner())
341        .is_some()
342}
343
344#[cfg(not(test))]
345fn store_oauth_key(key: &str) -> Result<(), String> {
346    car_inference::openrouter::store_oauth_credential(key).map_err(|error| error.to_string())
347}
348
349#[cfg(test)]
350fn store_oauth_key(key: &str) -> Result<(), String> {
351    *test_oauth_slot().lock().unwrap_or_else(|p| p.into_inner()) = Some(key.to_string());
352    Ok(())
353}
354
355#[cfg(not(test))]
356fn delete_oauth_key() -> Result<(), String> {
357    car_inference::openrouter::delete_oauth_credential().map_err(|error| error.to_string())
358}
359
360#[cfg(test)]
361fn delete_oauth_key() -> Result<(), String> {
362    *test_oauth_slot().lock().unwrap_or_else(|p| p.into_inner()) = None;
363    Ok(())
364}
365
366fn credential_presence_from_vault() -> CredentialPresence {
367    CredentialPresence {
368        environment: car_inference::openrouter::environment_key_exists(),
369        pasted: car_inference::openrouter::pasted_key_exists(),
370        oauth: oauth_key_exists(),
371    }
372}
373
374fn status_from_manager_with_presence(guard: &Manager, presence: CredentialPresence) -> Status {
375    let effective = if presence.environment {
376        Some(car_inference::openrouter::CredentialSource::Env)
377    } else if presence.pasted {
378        Some(car_inference::openrouter::CredentialSource::Pasted)
379    } else if presence.oauth {
380        Some(car_inference::openrouter::CredentialSource::Oauth)
381    } else {
382        None
383    };
384    let pending = guard.pending.as_ref().map(|flow| PendingStatus {
385        flow_id: flow.flow_id.clone(),
386        deadline_unix_ms: flow.deadline_unix_ms,
387    });
388    Status {
389        authority_generation: guard.action_generation,
390        state: if pending.is_some() {
391            "pending"
392        } else if effective.is_some() || presence.oauth {
393            "connected"
394        } else {
395            "idle"
396        }
397        .to_string(),
398        effective_source: effective
399            .map(|source| source.as_str().to_string())
400            .or_else(|| presence.oauth.then(|| "oauth".to_string()))
401            .unwrap_or_else(|| "none".to_string()),
402        pasted_key_exists: presence.pasted,
403        oauth_key_exists: presence.oauth,
404        pending,
405        last_result: guard.last_result.clone(),
406    }
407}
408
409fn status_from_manager(guard: &Manager) -> Status {
410    status_from_manager_with_presence(guard, guard.credentials)
411}
412
413pub fn status() -> Status {
414    // Reconcile the bounded credential-presence mirror on explicit status
415    // reads, but never hold the manager lock during OS keychain IO. The
416    // generation+revision compare prevents a delayed read from overwriting a
417    // newer daemon mutation or same-generation vault completion.
418    let expected = {
419        let guard = manager().lock().unwrap_or_else(|p| p.into_inner());
420        (guard.action_generation, guard.credential_revision)
421    };
422    let observed = credential_presence_from_vault();
423    let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
424    if (guard.action_generation, guard.credential_revision) == expected {
425        guard.replace_credential_presence(observed);
426    }
427    status_from_manager(&guard)
428}
429
430fn complete_pasted_change(reserved_generation: u64, exists_after_success: Option<bool>) -> Status {
431    let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
432    let is_current = guard.action_generation == reserved_generation;
433    if is_current {
434        if let Some(exists) = exists_after_success {
435            guard.set_pasted_exists(exists);
436        }
437    }
438    let status = status_from_manager(&guard);
439    drop(guard);
440    if is_current {
441        publish_status(&status);
442    }
443    status
444}
445
446pub fn is_pending(flow_id: &str) -> bool {
447    manager()
448        .lock()
449        .unwrap_or_else(|p| p.into_inner())
450        .pending
451        .as_ref()
452        .is_some_and(|pending| pending.flow_id == flow_id)
453}
454
455/// Return a terminal status only when it belongs to `flow_id` and no
456/// replacement flow is pending. The WebSocket watcher uses this instead of a
457/// generic status read so watcher A can never emit watcher B's pending state.
458pub fn terminal_status_for_flow(flow_id: &str) -> Option<Status> {
459    terminal_status_for_flow_with_hook(flow_id, ManagerTransactionHook::default())
460}
461
462fn terminal_status_for_flow_with_hook(
463    flow_id: &str,
464    hook: ManagerTransactionHook,
465) -> Option<Status> {
466    hook.before_lock();
467    let guard = manager().lock().unwrap_or_else(|p| p.into_inner());
468    let matches_terminal = guard.pending.is_none()
469        && guard
470            .last_result
471            .as_ref()
472            .is_some_and(|result| result.flow_id == flow_id);
473    if !matches_terminal {
474        return None;
475    }
476    hook.inside_lock();
477    Some(status_from_manager(&guard))
478}
479
480fn replace_pending_result(kind: &str, message: &str) -> Status {
481    let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
482    guard.advance_action_generation();
483    if let Some(flow) = guard.pending.take() {
484        flow.cancel.cancel();
485        guard.last_result = Some(terminal(&flow.flow_id, kind, message));
486    }
487    let status = status_from_manager(&guard);
488    drop(guard);
489    publish_status(&status);
490    status
491}
492
493/// Called by the generic secret surface before a pasted OpenRouter vault
494/// mutation. Superseding first prevents an older OAuth setup/finish from
495/// becoming authoritative while the blocking OS vault operation is in flight;
496/// a later vault failure never resurrects the invalidated flow.
497fn supersede_for_pasted_change() -> Status {
498    replace_pending_result(
499        "superseded",
500        "OpenRouter connect was superseded by a pasted-key change",
501    )
502}
503
504/// Serialize a pasted-key vault mutation, reserving/publishing its authority
505/// only after it owns the physical credential-IO gate. Same-slot operations
506/// therefore have the same generation and vault order. A completion superseded
507/// by a non-vault authority action returns current status but emits nothing.
508pub async fn mutate_pasted_credential<T, F>(
509    exists_on_success: bool,
510    operation: F,
511) -> (Result<T, String>, Status)
512where
513    F: FnOnce() -> Result<T, String>,
514{
515    mutate_pasted_credential_with_hook(exists_on_success, operation, PastedMutationHook::default())
516        .await
517}
518
519async fn mutate_pasted_credential_with_hook<T, F>(
520    exists_on_success: bool,
521    operation: F,
522    hook: PastedMutationHook,
523) -> (Result<T, String>, Status)
524where
525    F: FnOnce() -> Result<T, String>,
526{
527    hook.before_gate();
528    let _credential_io = credential_io_gate().lock().await;
529    let observed = credential_presence_from_vault();
530    manager()
531        .lock()
532        .unwrap_or_else(|p| p.into_inner())
533        .replace_credential_presence(observed);
534    let reserved = supersede_for_pasted_change();
535    let result = operation();
536    let final_status = complete_pasted_change(
537        reserved.authority_generation,
538        result.is_ok().then_some(exists_on_success),
539    );
540    (result, final_status)
541}
542
543pub fn cancel(flow_id: Option<&str>) -> Status {
544    let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
545    let matches = guard
546        .pending
547        .as_ref()
548        .is_some_and(|flow| flow_id.is_none_or(|id| id == flow.flow_id));
549    if matches {
550        guard.advance_action_generation();
551        let flow = guard.pending.take().expect("checked above");
552        flow.cancel.cancel();
553        guard.last_result = Some(terminal(
554            &flow.flow_id,
555            "cancelled",
556            "OpenRouter connect was cancelled",
557        ));
558        let status = status_from_manager(&guard);
559        drop(guard);
560        publish_status(&status);
561        return status;
562    }
563    let status = status_from_manager(&guard);
564    drop(guard);
565    status
566}
567
568pub async fn disconnect() -> Result<Status, String> {
569    disconnect_with_hook(DisconnectHook::default()).await
570}
571
572async fn disconnect_with_hook(hook: DisconnectHook) -> Result<Status, String> {
573    // Serialize physical credential mutations before reserving authority. A
574    // later same-slot mutation cannot claim a newer generation and then lose
575    // the physical vault race to this operation.
576    let _credential_io = credential_io_gate().lock().await;
577    let observed = credential_presence_from_vault();
578    let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
579    guard.replace_credential_presence(observed);
580    let reserved_generation = guard.advance_action_generation();
581    if let Some(flow) = guard.pending.take() {
582        flow.cancel.cancel();
583        guard.last_result = Some(terminal(
584            &flow.flow_id,
585            "cancelled",
586            "OpenRouter connect was cancelled",
587        ));
588    }
589    let reserved_status = status_from_manager(&guard);
590    drop(guard);
591    publish_status(&reserved_status);
592
593    let delete_result = hook
594        .delete_oauth_key()
595        .map_err(|error| format!("remove OpenRouter OAuth credential: {error}"));
596    let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
597    let is_current = guard.action_generation == reserved_generation;
598    if is_current && delete_result.is_ok() {
599        guard.set_oauth_exists(false);
600    }
601    let final_status = status_from_manager(&guard);
602    drop(guard);
603    if is_current {
604        publish_status(&final_status);
605    }
606    delete_result.map(|()| final_status)
607}
608
609pub async fn start(
610    authorization_base_url: Option<&str>,
611    exchange_url: Option<&str>,
612    timeout_seconds: Option<u64>,
613) -> Result<StartResult, String> {
614    start_with_hook(
615        authorization_base_url,
616        exchange_url,
617        timeout_seconds,
618        ManagerTransactionHook::default(),
619    )
620    .await
621}
622
623const SUPERSEDED_START_ERROR: &str =
624    "superseded: OpenRouter connect was replaced by a newer credential action";
625
626fn test_mode_enabled() -> bool {
627    std::env::var("CAR_OPENROUTER_TEST_MODE").as_deref() == Ok("1")
628}
629
630fn non_empty_environment_value(key: &str) -> Option<String> {
631    std::env::var(key)
632        .ok()
633        .map(|value| value.trim().to_string())
634        .filter(|value| !value.is_empty())
635}
636
637/// Resolve OAuth endpoints for one daemon-owned flow. Explicit RPC overrides
638/// remain a test-only contract. Environment overrides exist so the exact
639/// native button (which intentionally sends empty params) can drive a local
640/// OAuth double, and are ignored unless the daemon explicitly opted into test
641/// mode.
642fn resolve_start_endpoints(
643    authorization_base_url: Option<&str>,
644    exchange_url: Option<&str>,
645) -> Result<(String, String), String> {
646    let explicit_override = authorization_base_url.is_some() || exchange_url.is_some();
647    let test_mode = test_mode_enabled();
648    if explicit_override && !cfg!(test) && !test_mode {
649        return Err("OpenRouter endpoint overrides require CAR_OPENROUTER_TEST_MODE=1".into());
650    }
651    let authorization_base_url = authorization_base_url
652        .map(str::to_string)
653        .or_else(|| {
654            test_mode.then(|| non_empty_environment_value(TEST_AUTHORIZATION_BASE_URL_ENV))?
655        })
656        .unwrap_or_else(|| DEFAULT_AUTH_URL.to_string());
657    let exchange_url = exchange_url
658        .map(str::to_string)
659        .or_else(|| test_mode.then(|| non_empty_environment_value(TEST_EXCHANGE_URL_ENV))?)
660        .unwrap_or_else(|| DEFAULT_EXCHANGE_URL.to_string());
661    Ok((authorization_base_url, exchange_url))
662}
663
664fn start_reservation_is_current(action_generation: u64, flow_id: &str) -> bool {
665    let guard = manager().lock().unwrap_or_else(|p| p.into_inner());
666    guard.action_generation == action_generation
667        && guard.pending.as_ref().is_some_and(|pending| {
668            pending.action_generation == action_generation && pending.flow_id == flow_id
669        })
670}
671
672fn fail_current_start_preparation(action_generation: u64, flow_id: &str, message: &str) {
673    let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
674    let is_current = guard.action_generation == action_generation
675        && guard.pending.as_ref().is_some_and(|pending| {
676            pending.action_generation == action_generation && pending.flow_id == flow_id
677        });
678    if !is_current {
679        return;
680    }
681    if let Some(flow) = guard.pending.take() {
682        flow.cancel.cancel();
683    }
684    guard.last_result = Some(terminal(flow_id, "exchange_rejected", message));
685    let status = status_from_manager(&guard);
686    drop(guard);
687    publish_status(&status);
688}
689
690async fn start_with_hook(
691    authorization_base_url: Option<&str>,
692    exchange_url: Option<&str>,
693    timeout_seconds: Option<u64>,
694    transaction_hook: ManagerTransactionHook,
695) -> Result<StartResult, String> {
696    let (authorization_base_url, exchange_url) =
697        resolve_start_endpoints(authorization_base_url, exchange_url)?;
698
699    let flow_id = uuid::Uuid::new_v4().to_string();
700    let verifier = car_auth::pkce_verifier();
701    let challenge = car_auth::pkce_challenge(&verifier);
702    let timeout = timeout_seconds
703        .unwrap_or(DEFAULT_TIMEOUT_SECS)
704        .clamp(1, MAX_TIMEOUT_SECS);
705    let deadline_unix_ms = now_unix_ms().saturating_add(timeout * 1000);
706    let cancel = CancellationToken::new();
707    let (action_generation, reserved_status) = {
708        transaction_hook.before_lock();
709        let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
710        transaction_hook.inside_lock();
711        let action_generation = guard.advance_action_generation();
712        if let Some(flow) = guard.pending.take() {
713            flow.cancel.cancel();
714        }
715        guard.pending = Some(PendingFlow {
716            flow_id: flow_id.clone(),
717            action_generation,
718            deadline_unix_ms,
719            cancel: cancel.clone(),
720        });
721        guard.last_result = None;
722        (action_generation, status_from_manager(&guard))
723    };
724    publish_status(&reserved_status);
725
726    // Listener setup is awaited only after this action owns the manager epoch.
727    // A newer start/cancel/disconnect/pasted-key change can now invalidate it
728    // even while bind or PKCE preparation is still in progress.
729    transaction_hook.after_reserve();
730    if !start_reservation_is_current(action_generation, &flow_id) {
731        return Err(SUPERSEDED_START_ERROR.into());
732    }
733
734    let listener = match TcpListener::bind("127.0.0.1:0").await {
735        Ok(listener) => listener,
736        Err(error) => {
737            let message = format!("bind OpenRouter OAuth callback: {error}");
738            fail_current_start_preparation(action_generation, &flow_id, &message);
739            return Err(message);
740        }
741    };
742    let port = match listener.local_addr() {
743        Ok(address) => address.port(),
744        Err(error) => {
745            let message = format!("read OpenRouter OAuth callback address: {error}");
746            fail_current_start_preparation(action_generation, &flow_id, &message);
747            return Err(message);
748        }
749    };
750    let callback_url = format!("http://127.0.0.1:{port}/openrouter/callback/{flow_id}");
751    let mut auth_url = match reqwest::Url::parse(&authorization_base_url) {
752        Ok(url) => url,
753        Err(error) => {
754            let message = format!("invalid OpenRouter authorization URL: {error}");
755            fail_current_start_preparation(action_generation, &flow_id, &message);
756            return Err(message);
757        }
758    };
759    auth_url
760        .query_pairs_mut()
761        .append_pair("callback_url", &callback_url)
762        .append_pair("code_challenge", &challenge)
763        .append_pair("code_challenge_method", "S256");
764
765    if !start_reservation_is_current(action_generation, &flow_id) {
766        cancel.cancel();
767        return Err(SUPERSEDED_START_ERROR.into());
768    }
769
770    // A newer client may reserve authority after the final setup check but
771    // before this result is delivered. The returned generation makes that
772    // delayed result explicitly lower-authority, while the reservation
773    // broadcast lets every host converge to the newer snapshot first.
774    transaction_hook.before_return();
775
776    let task_flow_id = flow_id.clone();
777    tokio::spawn(async move {
778        run_flow(
779            listener,
780            task_flow_id,
781            verifier,
782            exchange_url,
783            Duration::from_secs(timeout),
784            cancel,
785        )
786        .await;
787    });
788
789    Ok(StartResult {
790        authority_generation: action_generation,
791        authorize_url: auth_url.to_string(),
792        flow_id,
793        deadline_unix_ms,
794    })
795}
796
797async fn run_flow(
798    listener: TcpListener,
799    flow_id: String,
800    verifier: String,
801    exchange_url: String,
802    timeout: Duration,
803    cancel: CancellationToken,
804) {
805    let deadline = tokio::time::Instant::now() + timeout;
806    let accepted = tokio::select! {
807        _ = cancel.cancelled() => return,
808        _ = tokio::time::sleep_until(deadline) => {
809            finish(&flow_id, terminal(&flow_id, "timed_out", "OpenRouter connect timed out"), None);
810            return;
811        }
812        accepted = listener.accept() => accepted,
813    };
814    let Ok((mut stream, _)) = accepted else {
815        finish(
816            &flow_id,
817            terminal(&flow_id, "exchange_rejected", "OpenRouter callback failed"),
818            None,
819        );
820        return;
821    };
822    let callback_read_deadline = std::cmp::min(
823        deadline,
824        tokio::time::Instant::now() + Duration::from_secs(5),
825    );
826    let request = match read_callback_headers(&mut stream, callback_read_deadline, &cancel).await {
827        Ok(bytes) => String::from_utf8_lossy(&bytes).to_string(),
828        Err(CallbackReadError::Cancelled) => return,
829        Err(CallbackReadError::TimedOut) => {
830            finish(
831                &flow_id,
832                terminal(&flow_id, "timed_out", "OpenRouter connect timed out"),
833                None,
834            );
835            return;
836        }
837        Err(CallbackReadError::TooLarge) => {
838            finish(
839                &flow_id,
840                terminal(
841                    &flow_id,
842                    "exchange_rejected",
843                    "OpenRouter callback was too large",
844                ),
845                None,
846            );
847            return;
848        }
849        Err(CallbackReadError::Incomplete) => {
850            finish(
851                &flow_id,
852                terminal(
853                    &flow_id,
854                    "exchange_rejected",
855                    "OpenRouter callback was incomplete",
856                ),
857                None,
858            );
859            return;
860        }
861        Err(CallbackReadError::Io) => {
862            finish(
863                &flow_id,
864                terminal(
865                    &flow_id,
866                    "exchange_rejected",
867                    "OpenRouter callback was invalid",
868                ),
869                None,
870            );
871            return;
872        }
873    };
874    let target = request
875        .lines()
876        .next()
877        .and_then(|line| line.split_whitespace().nth(1))
878        .unwrap_or("/");
879    let parsed = reqwest::Url::parse(&format!("http://127.0.0.1{target}"));
880    let (code, callback_error) = match parsed {
881        Ok(url) if url.path() == format!("/openrouter/callback/{flow_id}") => {
882            let pairs: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
883            (pairs.get("code").cloned(), pairs.get("error").cloned())
884        }
885        _ => (None, Some("invalid_callback".to_string())),
886    };
887    let response = if code.is_some() {
888        "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n<!doctype html><title>CAR connected</title><p>OpenRouter connected. You can close this window.</p>"
889    } else {
890        "HTTP/1.1 400 Bad Request\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n<!doctype html><title>CAR not connected</title><p>OpenRouter was not connected. Return to CarHost and retry.</p>"
891    };
892    let _ = stream.write_all(response.as_bytes()).await;
893    let _ = stream.shutdown().await;
894
895    if let Some(error) = callback_error {
896        let kind = if error == "access_denied" {
897            "cancelled"
898        } else {
899            "exchange_rejected"
900        };
901        finish(
902            &flow_id,
903            terminal(&flow_id, kind, "OpenRouter did not approve the connection"),
904            None,
905        );
906        return;
907    }
908    let Some(code) = code else {
909        finish(
910            &flow_id,
911            terminal(
912                &flow_id,
913                "exchange_rejected",
914                "OpenRouter callback contained no code",
915            ),
916            None,
917        );
918        return;
919    };
920
921    let exchange = reqwest::Client::new()
922        .post(&exchange_url)
923        .json(&serde_json::json!({
924            "code": code,
925            "code_verifier": verifier,
926            "code_challenge_method": "S256",
927        }))
928        .send();
929    let exchanged = tokio::select! {
930        _ = cancel.cancelled() => return,
931        _ = tokio::time::sleep_until(deadline) => {
932            finish(&flow_id, terminal(&flow_id, "timed_out", "OpenRouter connect timed out"), None);
933            return;
934        }
935        response = exchange => response,
936    };
937    let response = match exchanged {
938        Ok(response) if response.status().is_success() => response,
939        _ => {
940            finish(
941                &flow_id,
942                terminal(
943                    &flow_id,
944                    "exchange_rejected",
945                    "OpenRouter rejected the code exchange",
946                ),
947                None,
948            );
949            return;
950        }
951    };
952    let payload: serde_json::Value = match tokio::select! {
953        _ = cancel.cancelled() => return,
954        _ = tokio::time::sleep_until(deadline) => {
955            finish(&flow_id, terminal(&flow_id, "timed_out", "OpenRouter connect timed out"), None);
956            return;
957        }
958        payload = response.json() => payload,
959    } {
960        Ok(payload) => payload,
961        Err(_) => {
962            finish(
963                &flow_id,
964                terminal(
965                    &flow_id,
966                    "exchange_rejected",
967                    "OpenRouter returned an invalid exchange response",
968                ),
969                None,
970            );
971            return;
972        }
973    };
974    let Some(key) = payload
975        .get("key")
976        .and_then(|value| value.as_str())
977        .filter(|key| !key.trim().is_empty())
978    else {
979        finish(
980            &flow_id,
981            terminal(
982                &flow_id,
983                "exchange_rejected",
984                "OpenRouter exchange returned no key",
985            ),
986            None,
987        );
988        return;
989    };
990    finish_with_key(
991        &flow_id,
992        terminal(&flow_id, "connected", "OpenRouter account connected"),
993        key,
994    )
995    .await;
996}
997
998const MAX_CALLBACK_HEADER_BYTES: usize = 8_192;
999
1000#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1001enum CallbackReadError {
1002    Cancelled,
1003    TimedOut,
1004    TooLarge,
1005    Incomplete,
1006    Io,
1007}
1008
1009async fn read_callback_headers(
1010    stream: &mut tokio::net::TcpStream,
1011    deadline: tokio::time::Instant,
1012    cancel: &CancellationToken,
1013) -> Result<Vec<u8>, CallbackReadError> {
1014    let mut request = Vec::with_capacity(1024);
1015    let mut chunk = [0u8; 1024];
1016    loop {
1017        let count = tokio::select! {
1018            _ = cancel.cancelled() => return Err(CallbackReadError::Cancelled),
1019            _ = tokio::time::sleep_until(deadline) => return Err(CallbackReadError::TimedOut),
1020            read = stream.read(&mut chunk) => read.map_err(|_| CallbackReadError::Io)?,
1021        };
1022        if count == 0 {
1023            return Err(CallbackReadError::Incomplete);
1024        }
1025        request.extend_from_slice(&chunk[..count]);
1026        if request.len() > MAX_CALLBACK_HEADER_BYTES {
1027            return Err(CallbackReadError::TooLarge);
1028        }
1029        if request.windows(4).any(|window| window == b"\r\n\r\n") {
1030            return Ok(request);
1031        }
1032    }
1033}
1034
1035fn finish(flow_id: &str, result: TerminalResult, key: Option<&str>) {
1036    debug_assert!(key.is_none(), "credential finishes use finish_with_key");
1037    let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
1038    if !guard.pending.as_ref().is_some_and(|flow| {
1039        flow.flow_id == flow_id && flow.action_generation == guard.action_generation
1040    }) {
1041        return;
1042    }
1043    guard.pending = None;
1044    guard.last_result = Some(result);
1045    let status = status_from_manager(&guard);
1046    drop(guard);
1047    publish_status(&status);
1048}
1049
1050async fn finish_with_key(flow_id: &str, result: TerminalResult, key: &str) {
1051    finish_with_key_with_hook(flow_id, result, key, CredentialFinishHook::default()).await;
1052}
1053
1054async fn finish_with_key_with_hook(
1055    flow_id: &str,
1056    result: TerminalResult,
1057    key: &str,
1058    hook: CredentialFinishHook,
1059) {
1060    // Wait behind disconnect/pasted IO, then recheck authority before storing.
1061    // The manager lock remains held through the synchronous store so a newer
1062    // start/cancel cannot supersede this flow mid-write.
1063    hook.before_gate();
1064    let _credential_io = credential_io_gate().lock().await;
1065    let observed = credential_presence_from_vault();
1066    hook.after_gate();
1067    let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
1068    if !guard.pending.as_ref().is_some_and(|flow| {
1069        flow.flow_id == flow_id && flow.action_generation == guard.action_generation
1070    }) {
1071        return;
1072    }
1073    guard.replace_credential_presence(observed);
1074    if store_oauth_key(key).is_err() {
1075        guard.pending = None;
1076        guard.last_result = Some(terminal(
1077            flow_id,
1078            "exchange_rejected",
1079            "OpenRouter connected but the OS keychain rejected the credential",
1080        ));
1081        let status = status_from_manager(&guard);
1082        drop(guard);
1083        publish_status(&status);
1084        return;
1085    }
1086    guard.set_oauth_exists(true);
1087    guard.pending = None;
1088    guard.last_result = Some(result);
1089    let status = status_from_manager(&guard);
1090    drop(guard);
1091    publish_status(&status);
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096    use super::*;
1097    use tokio::net::TcpListener;
1098
1099    fn oauth_test_lock() -> &'static tokio::sync::Mutex<()> {
1100        static LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
1101        LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
1102    }
1103
1104    fn reset_test_manager() {
1105        delete_oauth_key().unwrap();
1106        let mut guard = manager().lock().unwrap_or_else(|p| p.into_inner());
1107        if let Some(flow) = guard.pending.take() {
1108            flow.cancel.cancel();
1109        }
1110        guard.action_generation = 0;
1111        guard.last_result = None;
1112        guard.credentials.oauth = false;
1113        guard.credential_revision = 0;
1114    }
1115
1116    async fn exchange_server(
1117        status: u16,
1118        body: &'static str,
1119    ) -> (String, tokio::task::JoinHandle<String>) {
1120        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1121        let url = format!("http://{}/exchange", listener.local_addr().unwrap());
1122        let task = tokio::spawn(async move {
1123            let (mut stream, _) = listener.accept().await.unwrap();
1124            let mut request = Vec::new();
1125            let mut chunk = [0u8; 4096];
1126            loop {
1127                let count = stream.read(&mut chunk).await.unwrap();
1128                if count == 0 {
1129                    break;
1130                }
1131                request.extend_from_slice(&chunk[..count]);
1132                let text = String::from_utf8_lossy(&request);
1133                let Some(header_end) = text.find("\r\n\r\n") else {
1134                    continue;
1135                };
1136                let content_length = text[..header_end]
1137                    .lines()
1138                    .find_map(|line| {
1139                        let (name, value) = line.split_once(':')?;
1140                        name.eq_ignore_ascii_case("content-length")
1141                            .then(|| value.trim().parse::<usize>().ok())
1142                            .flatten()
1143                    })
1144                    .unwrap_or(0);
1145                if request.len() >= header_end + 4 + content_length {
1146                    break;
1147                }
1148            }
1149            let reason = if status == 200 { "OK" } else { "Unauthorized" };
1150            let response = format!(
1151                "HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1152                body.len()
1153            );
1154            stream.write_all(response.as_bytes()).await.unwrap();
1155            String::from_utf8(request).unwrap()
1156        });
1157        (url, task)
1158    }
1159
1160    async fn wait_for_terminal(flow_id: &str) -> Status {
1161        for _ in 0..250 {
1162            let current = status();
1163            if current
1164                .pending
1165                .as_ref()
1166                .is_none_or(|pending| pending.flow_id != flow_id)
1167            {
1168                return current;
1169            }
1170            tokio::time::sleep(Duration::from_millis(25)).await;
1171        }
1172        panic!("OpenRouter flow {flow_id} did not finish");
1173    }
1174
1175    fn spawn_paused_preparing_start() -> (
1176        tokio::task::JoinHandle<Result<StartResult, String>>,
1177        String,
1178        Arc<std::sync::Barrier>,
1179    ) {
1180        let reserved = Arc::new(std::sync::Barrier::new(2));
1181        let release = Arc::new(std::sync::Barrier::new(2));
1182        let hook = ManagerTransactionHook {
1183            after_reserve_entered: Some(reserved.clone()),
1184            after_reserve_release: Some(release.clone()),
1185            ..ManagerTransactionHook::default()
1186        };
1187        let task = tokio::spawn(async move {
1188            start_with_hook(
1189                Some("http://127.0.0.1:9/auth"),
1190                Some("http://127.0.0.1:9/exchange"),
1191                Some(30),
1192                hook,
1193            )
1194            .await
1195        });
1196        tokio::task::block_in_place(|| reserved.wait());
1197        let flow_id = status()
1198            .pending
1199            .expect("preparing start owns pending authority")
1200            .flow_id;
1201        (task, flow_id, release)
1202    }
1203
1204    #[tokio::test]
1205    async fn empty_param_start_uses_local_endpoints_only_in_explicit_test_mode() {
1206        let _test_guard = oauth_test_lock().lock().await;
1207        reset_test_manager();
1208        let (exchange_url, exchange_task) =
1209            exchange_server(200, r#"{"key":"local-oauth-test-key"}"#).await;
1210        unsafe {
1211            std::env::set_var("CAR_OPENROUTER_TEST_MODE", "1");
1212            std::env::set_var(
1213                "OPENROUTER_AUTHORIZATION_BASE_URL",
1214                "http://127.0.0.1:9/local-openrouter-auth",
1215            );
1216            std::env::set_var("OPENROUTER_EXCHANGE_URL", &exchange_url);
1217        }
1218
1219        let started = start(None, None, Some(5)).await.unwrap();
1220        assert!(
1221            started
1222                .authorize_url
1223                .starts_with("http://127.0.0.1:9/local-openrouter-auth?"),
1224            "the exact native empty-params action must use the daemon test-mode override: {}",
1225            started.authorize_url
1226        );
1227        let authorize_url = reqwest::Url::parse(&started.authorize_url).unwrap();
1228        let callback_url = authorize_url
1229            .query_pairs()
1230            .find_map(|(name, value)| (name == "callback_url").then(|| value.into_owned()))
1231            .unwrap();
1232        reqwest::get(format!("{callback_url}?code=local-approved-code"))
1233            .await
1234            .unwrap();
1235        let terminal = wait_for_terminal(&started.flow_id).await;
1236        assert_eq!(terminal.state, "connected");
1237        let exchange_request = exchange_task.await.unwrap();
1238        assert!(exchange_request.starts_with("POST /exchange "));
1239        assert!(exchange_request.contains("local-approved-code"));
1240        assert!(exchange_request.contains("code_verifier"));
1241        disconnect().await.unwrap();
1242
1243        unsafe {
1244            std::env::remove_var("CAR_OPENROUTER_TEST_MODE");
1245        }
1246        let production = start(None, None, Some(5)).await.unwrap();
1247        assert!(
1248            production
1249                .authorize_url
1250                .starts_with("https://openrouter.ai/auth?"),
1251            "unsafe endpoint env vars must be ignored outside explicit test mode: {}",
1252            production.authorize_url
1253        );
1254        assert_eq!(
1255            resolve_start_endpoints(None, None).unwrap().1,
1256            DEFAULT_EXCHANGE_URL
1257        );
1258        cancel(Some(&production.flow_id));
1259
1260        unsafe {
1261            std::env::remove_var("OPENROUTER_EXCHANGE_URL");
1262            std::env::remove_var("OPENROUTER_AUTHORIZATION_BASE_URL");
1263        }
1264    }
1265
1266    #[tokio::test]
1267    async fn pkce_flow_stores_real_key_and_terminal_states_are_bounded() {
1268        let _test_guard = oauth_test_lock().lock().await;
1269        reset_test_manager();
1270
1271        let marker = "sk-or-v1-test-marker-never-serialize";
1272        let exchange_body: &'static str = Box::leak(
1273            serde_json::json!({ "key": marker })
1274                .to_string()
1275                .into_boxed_str(),
1276        );
1277        let (exchange_url, exchange_task) = exchange_server(200, exchange_body).await;
1278        let started = start(
1279            Some("http://127.0.0.1:9/auth"),
1280            Some(&exchange_url),
1281            Some(5),
1282        )
1283        .await
1284        .unwrap();
1285        let url = reqwest::Url::parse(&started.authorize_url).unwrap();
1286        let query: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
1287        assert_eq!(
1288            query.get("code_challenge_method").map(String::as_str),
1289            Some("S256")
1290        );
1291        assert!(query.get("code_challenge").is_some_and(|v| v.len() >= 43));
1292        assert!(query
1293            .get("callback_url")
1294            .is_some_and(|v| v.contains("127.0.0.1") && v.contains(&started.flow_id)));
1295        assert_eq!(status().state, "pending");
1296
1297        let callback = reqwest::Url::parse(query.get("callback_url").unwrap()).unwrap();
1298        let mut callback_stream = tokio::net::TcpStream::connect((
1299            callback.host_str().unwrap(),
1300            callback.port().unwrap(),
1301        ))
1302        .await
1303        .unwrap();
1304        for fragment in [
1305            "GET ".to_string(),
1306            format!("{}?code=approved-code HTTP/1.1\r\n", callback.path()),
1307            "Host: 127.0.0.1\r\n".to_string(),
1308            "Connection: close\r\n\r\n".to_string(),
1309        ] {
1310            callback_stream
1311                .write_all(fragment.as_bytes())
1312                .await
1313                .unwrap();
1314            tokio::task::yield_now().await;
1315        }
1316        let mut callback_response = Vec::new();
1317        callback_stream
1318            .read_to_end(&mut callback_response)
1319            .await
1320            .unwrap();
1321        assert!(
1322            String::from_utf8_lossy(&callback_response).starts_with("HTTP/1.1 200"),
1323            "{}",
1324            String::from_utf8_lossy(&callback_response)
1325        );
1326        let terminal = wait_for_terminal(&started.flow_id).await;
1327        assert_eq!(
1328            terminal.last_result.as_ref().unwrap().kind,
1329            "connected",
1330            "{}",
1331            terminal.last_result.as_ref().unwrap().message
1332        );
1333        assert!(terminal.oauth_key_exists);
1334        assert_eq!(
1335            test_oauth_slot()
1336                .lock()
1337                .unwrap_or_else(|p| p.into_inner())
1338                .as_deref(),
1339            Some(marker)
1340        );
1341        assert!(!serde_json::to_string(&terminal).unwrap().contains(marker));
1342        let exchange_request = exchange_task.await.unwrap();
1343        assert!(exchange_request.contains("approved-code"));
1344        assert!(exchange_request.contains("code_verifier"));
1345        assert!(exchange_request.contains("S256"));
1346
1347        let disconnected = disconnect().await.unwrap();
1348        assert!(!disconnected.oauth_key_exists);
1349        assert!(test_oauth_slot()
1350            .lock()
1351            .unwrap_or_else(|p| p.into_inner())
1352            .is_none());
1353
1354        let (reject_url, _reject_task) = exchange_server(401, "{}").await;
1355        let rejected = start(Some("http://127.0.0.1:9/auth"), Some(&reject_url), Some(5))
1356            .await
1357            .unwrap();
1358        let rejected_url = reqwest::Url::parse(&rejected.authorize_url).unwrap();
1359        let rejected_callback = rejected_url
1360            .query_pairs()
1361            .find_map(|(name, value)| (name == "callback_url").then(|| value.into_owned()))
1362            .unwrap();
1363        reqwest::get(format!("{rejected_callback}?code=rejected-code"))
1364            .await
1365            .unwrap();
1366        assert_eq!(
1367            wait_for_terminal(&rejected.flow_id)
1368                .await
1369                .last_result
1370                .unwrap()
1371                .kind,
1372            "exchange_rejected"
1373        );
1374
1375        let timed = start(
1376            Some("http://127.0.0.1:9/auth"),
1377            Some("http://127.0.0.1:9/exchange"),
1378            Some(1),
1379        )
1380        .await
1381        .unwrap();
1382        assert_eq!(
1383            wait_for_terminal(&timed.flow_id)
1384                .await
1385                .last_result
1386                .unwrap()
1387                .kind,
1388            "timed_out"
1389        );
1390
1391        let cancelled = start(
1392            Some("http://127.0.0.1:9/auth"),
1393            Some("http://127.0.0.1:9/exchange"),
1394            Some(5),
1395        )
1396        .await
1397        .unwrap();
1398        assert_eq!(
1399            cancel(Some(&cancelled.flow_id)).last_result.unwrap().kind,
1400            "cancelled"
1401        );
1402
1403        let first = start(
1404            Some("http://127.0.0.1:9/auth"),
1405            Some("http://127.0.0.1:9/exchange"),
1406            Some(5),
1407        )
1408        .await
1409        .unwrap();
1410        let replacement = start(
1411            Some("http://127.0.0.1:9/auth"),
1412            Some("http://127.0.0.1:9/exchange"),
1413            Some(5),
1414        )
1415        .await
1416        .unwrap();
1417        let replacement_status = status();
1418        assert_eq!(replacement_status.state, "pending");
1419        assert_eq!(
1420            replacement_status
1421                .pending
1422                .as_ref()
1423                .map(|p| p.flow_id.as_str()),
1424            Some(replacement.flow_id.as_str())
1425        );
1426        assert!(
1427            terminal_status_for_flow(&first.flow_id).is_none(),
1428            "superseded watcher A must not emit replacement flow B's pending state"
1429        );
1430        let replacement_terminal = cancel(Some(&replacement.flow_id));
1431        assert_eq!(
1432            replacement_terminal
1433                .last_result
1434                .as_ref()
1435                .map(|r| r.flow_id.as_str()),
1436            Some(replacement.flow_id.as_str())
1437        );
1438        assert!(terminal_status_for_flow(&replacement.flow_id).is_some());
1439
1440        let superseded = start(
1441            Some("http://127.0.0.1:9/auth"),
1442            Some("http://127.0.0.1:9/exchange"),
1443            Some(5),
1444        )
1445        .await
1446        .unwrap();
1447        supersede_for_pasted_change();
1448        let superseded_status = wait_for_terminal(&superseded.flow_id).await;
1449        assert_eq!(superseded_status.last_result.unwrap().kind, "superseded");
1450
1451        delete_oauth_key().unwrap();
1452    }
1453
1454    #[tokio::test]
1455    async fn callback_rejects_oversize_and_times_out_incomplete_headers() {
1456        let _test_guard = oauth_test_lock().lock().await;
1457        reset_test_manager();
1458
1459        let oversized = start(
1460            Some("http://127.0.0.1:9/auth"),
1461            Some("http://127.0.0.1:9/exchange"),
1462            Some(5),
1463        )
1464        .await
1465        .unwrap();
1466        let oversized_url = reqwest::Url::parse(&oversized.authorize_url)
1467            .unwrap()
1468            .query_pairs()
1469            .find_map(|(name, value)| (name == "callback_url").then(|| value.into_owned()))
1470            .unwrap();
1471        let oversized_url = reqwest::Url::parse(&oversized_url).unwrap();
1472        let mut stream = tokio::net::TcpStream::connect((
1473            oversized_url.host_str().unwrap(),
1474            oversized_url.port().unwrap(),
1475        ))
1476        .await
1477        .unwrap();
1478        stream.write_all(&vec![b'x'; 8_193]).await.unwrap();
1479        let oversized_terminal = wait_for_terminal(&oversized.flow_id).await;
1480        let oversized_result = oversized_terminal.last_result.unwrap();
1481        assert_eq!(oversized_result.kind, "exchange_rejected");
1482        assert_eq!(
1483            oversized_result.message,
1484            "OpenRouter callback was too large"
1485        );
1486
1487        let incomplete = start(
1488            Some("http://127.0.0.1:9/auth"),
1489            Some("http://127.0.0.1:9/exchange"),
1490            Some(10),
1491        )
1492        .await
1493        .unwrap();
1494        let incomplete_url = reqwest::Url::parse(&incomplete.authorize_url)
1495            .unwrap()
1496            .query_pairs()
1497            .find_map(|(name, value)| (name == "callback_url").then(|| value.into_owned()))
1498            .unwrap();
1499        let incomplete_url = reqwest::Url::parse(&incomplete_url).unwrap();
1500        let mut stream = tokio::net::TcpStream::connect((
1501            incomplete_url.host_str().unwrap(),
1502            incomplete_url.port().unwrap(),
1503        ))
1504        .await
1505        .unwrap();
1506        stream
1507            .write_all(b"GET /incomplete HTTP/1.1\r\n")
1508            .await
1509            .unwrap();
1510        let incomplete_terminal = wait_for_terminal(&incomplete.flow_id).await;
1511        assert_eq!(incomplete_terminal.last_result.unwrap().kind, "timed_out");
1512
1513        let half_closed = start(
1514            Some("http://127.0.0.1:9/auth"),
1515            Some("http://127.0.0.1:9/exchange"),
1516            Some(5),
1517        )
1518        .await
1519        .unwrap();
1520        let half_closed_url = reqwest::Url::parse(&half_closed.authorize_url)
1521            .unwrap()
1522            .query_pairs()
1523            .find_map(|(name, value)| (name == "callback_url").then(|| value.into_owned()))
1524            .unwrap();
1525        let half_closed_url = reqwest::Url::parse(&half_closed_url).unwrap();
1526        let mut stream = tokio::net::TcpStream::connect((
1527            half_closed_url.host_str().unwrap(),
1528            half_closed_url.port().unwrap(),
1529        ))
1530        .await
1531        .unwrap();
1532        stream
1533            .write_all(b"GET /half-close HTTP/1.1\r\nHost: localhost\r\n")
1534            .await
1535            .unwrap();
1536        stream.shutdown().await.unwrap();
1537        let half_closed_terminal = wait_for_terminal(&half_closed.flow_id).await;
1538        let half_closed_result = half_closed_terminal.last_result.unwrap();
1539        assert_eq!(half_closed_result.kind, "exchange_rejected");
1540        assert_eq!(
1541            half_closed_result.message,
1542            "OpenRouter callback was incomplete"
1543        );
1544    }
1545
1546    #[tokio::test]
1547    async fn callback_header_reader_rejects_half_closed_incomplete_request() {
1548        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1549        let address = listener.local_addr().unwrap();
1550        let client = tokio::spawn(async move {
1551            let mut stream = tokio::net::TcpStream::connect(address).await.unwrap();
1552            stream
1553                .write_all(b"GET /callback?code=partial HTTP/1.1\r\nHost: localhost\r\n")
1554                .await
1555                .unwrap();
1556            stream.shutdown().await.unwrap();
1557        });
1558        let (mut server, _) = listener.accept().await.unwrap();
1559        let result = read_callback_headers(
1560            &mut server,
1561            tokio::time::Instant::now() + Duration::from_secs(1),
1562            &CancellationToken::new(),
1563        )
1564        .await;
1565        client.await.unwrap();
1566        assert!(
1567            result.is_err(),
1568            "EOF before CRLFCRLF must be an invalid callback, not a parseable request"
1569        );
1570    }
1571
1572    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1573    async fn newer_start_supersedes_an_older_preparing_start() {
1574        let _test_guard = oauth_test_lock().lock().await;
1575        reset_test_manager();
1576
1577        let (first_task, first_flow_id, release_first) = spawn_paused_preparing_start();
1578        let second = start(
1579            Some("http://127.0.0.1:9/auth"),
1580            Some("http://127.0.0.1:9/exchange"),
1581            Some(30),
1582        )
1583        .await
1584        .unwrap();
1585        tokio::task::block_in_place(|| release_first.wait());
1586        let first_error = first_task.await.unwrap().unwrap_err();
1587
1588        assert!(first_error.contains("superseded"));
1589        let current = status();
1590        assert_eq!(
1591            current.pending.as_ref().map(|p| p.flow_id.as_str()),
1592            Some(second.flow_id.as_str())
1593        );
1594        assert!(!is_pending(&first_flow_id));
1595        assert!(is_pending(&second.flow_id));
1596        assert!(terminal_status_for_flow(&first_flow_id).is_none());
1597        cancel(Some(&second.flow_id));
1598    }
1599
1600    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1601    async fn delayed_start_result_is_lower_authority_than_newer_broadcast() {
1602        let _test_guard = oauth_test_lock().lock().await;
1603        reset_test_manager();
1604
1605        let before_return = Arc::new(std::sync::Barrier::new(2));
1606        let release_return = Arc::new(std::sync::Barrier::new(2));
1607        let hook = ManagerTransactionHook {
1608            before_return_entered: Some(before_return.clone()),
1609            before_return_release: Some(release_return.clone()),
1610            ..ManagerTransactionHook::default()
1611        };
1612        let mut updates = subscribe_authority_updates();
1613        let first_task = tokio::spawn(async move {
1614            start_with_hook(
1615                Some("http://127.0.0.1:9/auth"),
1616                Some("http://127.0.0.1:9/exchange"),
1617                Some(30),
1618                hook,
1619            )
1620            .await
1621            .unwrap()
1622        });
1623        tokio::task::block_in_place(|| before_return.wait());
1624
1625        let second = start(
1626            Some("http://127.0.0.1:9/auth"),
1627            Some("http://127.0.0.1:9/exchange"),
1628            Some(30),
1629        )
1630        .await
1631        .unwrap();
1632        updates.changed().await.unwrap();
1633        let broadcast = updates
1634            .borrow_and_update()
1635            .clone()
1636            .expect("newer authority snapshot");
1637        assert_eq!(broadcast.authority_generation, second.authority_generation);
1638        assert_eq!(
1639            broadcast
1640                .pending
1641                .as_ref()
1642                .map(|pending| pending.flow_id.as_str()),
1643            Some(second.flow_id.as_str())
1644        );
1645
1646        tokio::task::block_in_place(|| release_return.wait());
1647        let delayed_first = first_task.await.unwrap();
1648        assert!(
1649            delayed_first.authority_generation < broadcast.authority_generation,
1650            "host must reject delayed flow A after receiving flow B broadcast"
1651        );
1652        assert_ne!(delayed_first.flow_id, second.flow_id);
1653        cancel(Some(&second.flow_id));
1654    }
1655
1656    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1657    async fn disconnect_publishes_before_delete_and_preserves_truth_on_failure() {
1658        let _test_guard = oauth_test_lock().lock().await;
1659        reset_test_manager();
1660        *test_oauth_slot()
1661            .lock()
1662            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some("existing-oauth".into());
1663        manager()
1664            .lock()
1665            .unwrap_or_else(|poisoned| poisoned.into_inner())
1666            .set_oauth_exists(true);
1667
1668        let before_return = Arc::new(std::sync::Barrier::new(2));
1669        let release_return = Arc::new(std::sync::Barrier::new(2));
1670        let start_hook = ManagerTransactionHook {
1671            before_return_entered: Some(before_return.clone()),
1672            before_return_release: Some(release_return.clone()),
1673            ..ManagerTransactionHook::default()
1674        };
1675        let mut updates = subscribe_authority_updates();
1676        let start_task = tokio::spawn(async move {
1677            start_with_hook(
1678                Some("http://127.0.0.1:9/auth"),
1679                Some("http://127.0.0.1:9/exchange"),
1680                Some(30),
1681                start_hook,
1682            )
1683            .await
1684            .unwrap()
1685        });
1686        tokio::task::block_in_place(|| before_return.wait());
1687        updates.changed().await.unwrap();
1688        let start_snapshot = updates.borrow_and_update().clone().unwrap();
1689
1690        let before_delete = Arc::new(std::sync::Barrier::new(2));
1691        let release_delete = Arc::new(std::sync::Barrier::new(2));
1692        let disconnect_hook = DisconnectHook {
1693            before_delete_entered: Some(before_delete.clone()),
1694            before_delete_release: Some(release_delete.clone()),
1695            ..DisconnectHook::default()
1696        };
1697        let disconnect_task =
1698            tokio::spawn(async move { disconnect_with_hook(disconnect_hook).await });
1699        tokio::task::block_in_place(|| before_delete.wait());
1700
1701        // Reaching the deletion barrier proves the reserved generation was
1702        // already published without performing vault IO.
1703        updates.changed().await.unwrap();
1704        let reserved_disconnect = updates.borrow_and_update().clone().unwrap();
1705        assert!(reserved_disconnect.authority_generation > start_snapshot.authority_generation);
1706        assert!(reserved_disconnect.pending.is_none());
1707        assert!(reserved_disconnect.oauth_key_exists);
1708
1709        tokio::task::block_in_place(|| release_return.wait());
1710        let delayed_start = start_task.await.unwrap();
1711        assert!(
1712            delayed_start.authority_generation < reserved_disconnect.authority_generation,
1713            "a host must reject delayed flow A before disconnect enters the vault"
1714        );
1715
1716        tokio::task::block_in_place(|| release_delete.wait());
1717        let disconnected = disconnect_task.await.unwrap().unwrap();
1718        assert_eq!(
1719            disconnected.authority_generation,
1720            reserved_disconnect.authority_generation
1721        );
1722        assert!(!disconnected.oauth_key_exists);
1723
1724        // A failed deletion republishes the same generation with the credential
1725        // still present, while the prior flow remains permanently invalidated.
1726        *test_oauth_slot()
1727            .lock()
1728            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some("existing-oauth".into());
1729        manager()
1730            .lock()
1731            .unwrap_or_else(|poisoned| poisoned.into_inner())
1732            .set_oauth_exists(true);
1733        let generation_before_failure = status().authority_generation;
1734        let error = disconnect_with_hook(DisconnectHook {
1735            forced_delete_error: Some("permitted vault failure".into()),
1736            ..DisconnectHook::default()
1737        })
1738        .await
1739        .unwrap_err();
1740        assert!(error.contains("permitted vault failure"));
1741        let failed_status = status();
1742        assert_eq!(
1743            failed_status.authority_generation,
1744            generation_before_failure + 1
1745        );
1746        assert!(failed_status.oauth_key_exists);
1747        assert!(failed_status.pending.is_none());
1748    }
1749
1750    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1751    async fn disconnect_delete_finishes_before_newer_oauth_store() {
1752        let _test_guard = oauth_test_lock().lock().await;
1753        reset_test_manager();
1754        *test_oauth_slot()
1755            .lock()
1756            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some("old-oauth".into());
1757        manager()
1758            .lock()
1759            .unwrap_or_else(|poisoned| poisoned.into_inner())
1760            .set_oauth_exists(true);
1761
1762        let before_delete = Arc::new(std::sync::Barrier::new(2));
1763        let release_delete = Arc::new(std::sync::Barrier::new(2));
1764        let disconnect_hook = DisconnectHook {
1765            before_delete_entered: Some(before_delete.clone()),
1766            before_delete_release: Some(release_delete.clone()),
1767            ..DisconnectHook::default()
1768        };
1769        let disconnect_task =
1770            tokio::spawn(async move { disconnect_with_hook(disconnect_hook).await });
1771        tokio::task::block_in_place(|| before_delete.wait());
1772
1773        let newer = start(
1774            Some("http://127.0.0.1:9/auth"),
1775            Some("http://127.0.0.1:9/exchange"),
1776            Some(30),
1777        )
1778        .await
1779        .unwrap();
1780        let finish_waiting = Arc::new(std::sync::Barrier::new(2));
1781        let finish_entered = Arc::new(std::sync::Barrier::new(2));
1782        let release_finish = Arc::new(std::sync::Barrier::new(2));
1783        let finish_hook = CredentialFinishHook {
1784            before_gate_entered: Some(finish_waiting.clone()),
1785            after_gate_entered: Some(finish_entered.clone()),
1786            after_gate_release: Some(release_finish.clone()),
1787        };
1788        let newer_flow_id = newer.flow_id.clone();
1789        let finish_task = tokio::spawn(async move {
1790            finish_with_key_with_hook(
1791                &newer_flow_id,
1792                terminal(&newer_flow_id, "connected", "newer OAuth connected"),
1793                "new-oauth",
1794                finish_hook,
1795            )
1796            .await;
1797        });
1798
1799        // B owns the credential gate. C may reserve newer authority, but its
1800        // physical store cannot pass B's deletion.
1801        tokio::task::block_in_place(|| finish_waiting.wait());
1802        tokio::task::block_in_place(|| release_delete.wait());
1803        tokio::task::block_in_place(|| finish_entered.wait());
1804        let stale_disconnect = disconnect_task.await.unwrap().unwrap();
1805        assert_eq!(
1806            stale_disconnect.authority_generation,
1807            newer.authority_generation
1808        );
1809        assert_eq!(
1810            test_oauth_slot()
1811                .lock()
1812                .unwrap_or_else(|poisoned| poisoned.into_inner())
1813                .as_deref(),
1814            None,
1815            "disconnect deletion must complete before newer OAuth store enters"
1816        );
1817
1818        tokio::task::block_in_place(|| release_finish.wait());
1819        finish_task.await.unwrap();
1820        assert_eq!(
1821            test_oauth_slot()
1822                .lock()
1823                .unwrap_or_else(|poisoned| poisoned.into_inner())
1824                .as_deref(),
1825            Some("new-oauth")
1826        );
1827        let final_status = status();
1828        assert_eq!(
1829            final_status.authority_generation,
1830            newer.authority_generation
1831        );
1832        assert!(final_status.oauth_key_exists);
1833        assert_eq!(
1834            final_status
1835                .last_result
1836                .as_ref()
1837                .map(|result| (result.flow_id.as_str(), result.kind.as_str())),
1838            Some((newer.flow_id.as_str(), "connected"))
1839        );
1840    }
1841
1842    #[tokio::test]
1843    async fn status_read_reconciles_external_oauth_slot_changes() {
1844        let _test_guard = oauth_test_lock().lock().await;
1845        reset_test_manager();
1846
1847        *test_oauth_slot()
1848            .lock()
1849            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some("external-oauth".into());
1850        let connected = status();
1851        assert!(connected.oauth_key_exists);
1852
1853        *test_oauth_slot()
1854            .lock()
1855            .unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
1856        let removed = status();
1857        assert!(!removed.oauth_key_exists);
1858    }
1859
1860    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1861    async fn cancel_supersedes_an_older_preparing_start() {
1862        let _test_guard = oauth_test_lock().lock().await;
1863        reset_test_manager();
1864
1865        let (start_task, flow_id, release_start) = spawn_paused_preparing_start();
1866        let cancelled = cancel(Some(&flow_id));
1867        tokio::task::block_in_place(|| release_start.wait());
1868        let start_error = start_task.await.unwrap().unwrap_err();
1869
1870        assert!(start_error.contains("superseded"));
1871        assert!(cancelled.pending.is_none());
1872        assert_eq!(
1873            cancelled
1874                .last_result
1875                .as_ref()
1876                .map(|result| result.kind.as_str()),
1877            Some("cancelled")
1878        );
1879        assert!(!is_pending(&flow_id));
1880    }
1881
1882    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1883    async fn disconnect_supersedes_an_older_preparing_start() {
1884        let _test_guard = oauth_test_lock().lock().await;
1885        reset_test_manager();
1886
1887        let (start_task, flow_id, release_start) = spawn_paused_preparing_start();
1888        let disconnected = disconnect().await.unwrap();
1889        tokio::task::block_in_place(|| release_start.wait());
1890        let start_error = start_task.await.unwrap().unwrap_err();
1891
1892        assert!(start_error.contains("superseded"));
1893        assert!(disconnected.pending.is_none());
1894        assert!(!disconnected.oauth_key_exists);
1895        assert!(!is_pending(&flow_id));
1896    }
1897
1898    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1899    async fn pasted_put_and_delete_reserve_before_vault_and_supersede_preparing_start() {
1900        let _test_guard = oauth_test_lock().lock().await;
1901
1902        for operation in ["put", "delete"] {
1903            reset_test_manager();
1904            let (start_task, flow_id, release_start) = spawn_paused_preparing_start();
1905            let generation_before_mutation = status().authority_generation;
1906            let mut updates = subscribe_authority_updates();
1907            let vault_entered = Arc::new(std::sync::Barrier::new(2));
1908            let release_vault = Arc::new(std::sync::Barrier::new(2));
1909            let vault_entered_task = vault_entered.clone();
1910            let release_vault_task = release_vault.clone();
1911            let mutation_task = tokio::spawn(async move {
1912                mutate_pasted_credential(operation == "put", move || {
1913                    // Permitted vault double: B remains in-flight after its
1914                    // authority broadcast while holding the credential gate.
1915                    vault_entered_task.wait();
1916                    release_vault_task.wait();
1917                    Ok::<_, String>(())
1918                })
1919                .await
1920            });
1921            tokio::task::block_in_place(|| vault_entered.wait());
1922            updates.changed().await.unwrap();
1923            let reserved = updates.borrow_and_update().clone().unwrap();
1924            assert!(
1925                reserved.authority_generation > generation_before_mutation,
1926                "{operation} must broadcast before entering the vault"
1927            );
1928
1929            // Even an already-computed OAuth key from A cannot finish while B
1930            // is paused in its put/delete vault operation.
1931            let finish_flow_id = flow_id.clone();
1932            let finish_task = tokio::spawn(async move {
1933                finish_with_key(
1934                    &finish_flow_id,
1935                    terminal(&finish_flow_id, "connected", "stale OAuth finish"),
1936                    "test-oauth-key",
1937                )
1938                .await;
1939            });
1940            tokio::task::block_in_place(|| release_start.wait());
1941            let start_error = start_task.await.unwrap().unwrap_err();
1942            assert!(start_error.contains("superseded"), "{operation}");
1943            assert!(!is_pending(&flow_id), "{operation}");
1944            assert!(
1945                test_oauth_slot()
1946                    .lock()
1947                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1948                    .is_none(),
1949                "{operation} must reject A's OAuth key"
1950            );
1951            assert_eq!(
1952                status()
1953                    .last_result
1954                    .as_ref()
1955                    .map(|result| result.kind.as_str()),
1956                Some("superseded"),
1957                "{operation}"
1958            );
1959
1960            tokio::task::block_in_place(|| release_vault.wait());
1961            let (mutation_result, _) = mutation_task.await.unwrap();
1962            mutation_result.unwrap();
1963            finish_task.await.unwrap();
1964            assert!(
1965                test_oauth_slot()
1966                    .lock()
1967                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1968                    .is_none(),
1969                "{operation} must reject A's OAuth key after waiting for B's vault IO"
1970            );
1971        }
1972    }
1973
1974    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1975    async fn pasted_same_slot_generation_order_matches_physical_io_order() {
1976        let _test_guard = oauth_test_lock().lock().await;
1977
1978        for (first_exists, second_exists) in [(false, true), (true, false)] {
1979            reset_test_manager();
1980            let physical_slot = Arc::new(Mutex::new(if first_exists {
1981                None
1982            } else {
1983                Some("initial".to_string())
1984            }));
1985            let first_entered = Arc::new(std::sync::Barrier::new(2));
1986            let release_first = Arc::new(std::sync::Barrier::new(2));
1987            let first_slot = physical_slot.clone();
1988            let first_entered_task = first_entered.clone();
1989            let release_first_task = release_first.clone();
1990            let first_task = tokio::spawn(async move {
1991                mutate_pasted_credential(first_exists, move || {
1992                    first_entered_task.wait();
1993                    release_first_task.wait();
1994                    *first_slot
1995                        .lock()
1996                        .unwrap_or_else(|poisoned| poisoned.into_inner()) =
1997                        first_exists.then(|| "first".to_string());
1998                    Ok::<_, String>(())
1999                })
2000                .await
2001            });
2002            tokio::task::block_in_place(|| first_entered.wait());
2003            let first_generation = status().authority_generation;
2004
2005            let second_waiting = Arc::new(std::sync::Barrier::new(2));
2006            let second_slot = physical_slot.clone();
2007            let second_waiting_task = second_waiting.clone();
2008            let second_task = tokio::spawn(async move {
2009                mutate_pasted_credential_with_hook(
2010                    second_exists,
2011                    move || {
2012                        *second_slot
2013                            .lock()
2014                            .unwrap_or_else(|poisoned| poisoned.into_inner()) =
2015                            second_exists.then(|| "second".to_string());
2016                        Ok::<_, String>(())
2017                    },
2018                    PastedMutationHook {
2019                        before_gate_entered: Some(second_waiting_task),
2020                    },
2021                )
2022                .await
2023            });
2024
2025            // C cannot reserve until B completes its physical operation because
2026            // both acquire the credential gate before authority reservation.
2027            tokio::task::block_in_place(|| second_waiting.wait());
2028            assert_eq!(status().authority_generation, first_generation);
2029            tokio::task::block_in_place(|| release_first.wait());
2030            let (_, first_status) = first_task.await.unwrap();
2031            let (_, second_status) = second_task.await.unwrap();
2032
2033            assert_eq!(first_status.authority_generation, first_generation);
2034            assert!(second_status.authority_generation > first_generation);
2035            assert_eq!(second_status.pasted_key_exists, second_exists);
2036            assert_eq!(
2037                physical_slot
2038                    .lock()
2039                    .unwrap_or_else(|poisoned| poisoned.into_inner())
2040                    .as_deref(),
2041                second_exists.then_some("second")
2042            );
2043        }
2044    }
2045
2046    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2047    async fn watcher_snapshot_never_turns_flow_a_terminal_into_flow_b_status() {
2048        let _test_guard = oauth_test_lock().lock().await;
2049        reset_test_manager();
2050
2051        let first = start(
2052            Some("http://127.0.0.1:9/auth"),
2053            Some("http://127.0.0.1:9/exchange"),
2054            Some(30),
2055        )
2056        .await
2057        .unwrap();
2058        let cancelled = cancel(Some(&first.flow_id));
2059        assert_eq!(
2060            cancelled.last_result.as_ref().map(|r| r.flow_id.as_str()),
2061            Some(first.flow_id.as_str())
2062        );
2063
2064        let watcher_inside = Arc::new(std::sync::Barrier::new(2));
2065        let release_watcher = Arc::new(std::sync::Barrier::new(2));
2066        let watcher_hook = ManagerTransactionHook {
2067            inside_lock_entered: Some(watcher_inside.clone()),
2068            inside_lock_release: Some(release_watcher.clone()),
2069            ..ManagerTransactionHook::default()
2070        };
2071        let first_flow_id = first.flow_id.clone();
2072        let watcher = tokio::task::spawn_blocking(move || {
2073            terminal_status_for_flow_with_hook(&first_flow_id, watcher_hook)
2074        });
2075        tokio::task::block_in_place(|| watcher_inside.wait());
2076
2077        let second_ready = Arc::new(std::sync::Barrier::new(2));
2078        let second_hook = ManagerTransactionHook {
2079            before_lock: Some(second_ready.clone()),
2080            ..ManagerTransactionHook::default()
2081        };
2082        let second_task = tokio::spawn(async move {
2083            start_with_hook(
2084                Some("http://127.0.0.1:9/auth"),
2085                Some("http://127.0.0.1:9/exchange"),
2086                Some(30),
2087                second_hook,
2088            )
2089            .await
2090            .unwrap()
2091        });
2092        tokio::task::block_in_place(|| second_ready.wait());
2093        tokio::task::block_in_place(|| release_watcher.wait());
2094
2095        let captured = watcher.await.unwrap().expect("flow A terminal snapshot");
2096        let second = second_task.await.unwrap();
2097        assert!(captured.pending.is_none());
2098        assert_eq!(
2099            captured.last_result.as_ref().map(|r| r.flow_id.as_str()),
2100            Some(first.flow_id.as_str()),
2101            "watcher A must carry captured A terminal state, never global B state"
2102        );
2103        assert_eq!(
2104            status().pending.as_ref().map(|p| p.flow_id.as_str()),
2105            Some(second.flow_id.as_str())
2106        );
2107        assert!(terminal_status_for_flow(&first.flow_id).is_none());
2108        cancel(Some(&second.flow_id));
2109    }
2110}