1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::{Result, RuntimeError};
use reqwest::Client;
use super::types::{AuthState, PiAuth};
pub(super) struct AuthMethods;
impl AuthMethods {
/// Check if the OAuth token is expired and refresh it if needed.
/// Uses Pi-style file locking for cross-process safety:
/// - Acquires exclusive lock on auth.json
/// - Re-reads inside the lock (another instance may have refreshed)
/// - Refreshes via API only if still expired
/// - Writes back atomically and releases lock
///
/// Multiple SynapsCLI instances (or Avante/Jade) can safely call this
/// simultaneously — they'll serialize on the lock and only one will
/// actually hit the token endpoint.
pub(super) async fn refresh_if_needed(auth: Arc<RwLock<AuthState>>, client: &Client) -> Result<()> {
// Fast path: read lock to check expiry without blocking writers
{
let auth_guard = auth.read().await;
if auth_guard.auth_type != "oauth" {
return Ok(());
}
let in_memory_expired = match auth_guard.token_expires {
Some(exp) => {
let now = crate::epoch_millis();
now >= exp
}
None => false,
};
if !in_memory_expired {
return Ok(());
}
}
// Read lock dropped here
tracing::info!("Token needs refresh, checking...");
// Slow path: delegate to auth.rs which handles locking, re-read,
// conditional refresh, and persistence.
tracing::info!("Refreshing auth token");
let creds = crate::auth::ensure_fresh_token(client)
.await
.map_err(|e| RuntimeError::Auth(format!(
"Token refresh failed: {}. Run `login` to re-authenticate.", e
)))?;
// Update shared auth state so all clones (including spawned stream tasks)
// immediately see the fresh token.
{
let mut auth_guard = auth.write().await;
auth_guard.auth_token = creds.access;
auth_guard.refresh_token = Some(creds.refresh);
auth_guard.token_expires = Some(creds.expires);
}
Ok(())
}
pub(super) fn get_auth_token() -> Result<(String, String, Option<String>, Option<u64>)> {
// Try auth.json via the auth module
if let Ok(Some(auth_file)) = crate::auth::load_auth() {
let creds = &auth_file.anthropic;
if creds.auth_type == "oauth" && !creds.access.is_empty() {
return Ok((
creds.access.clone(),
"oauth".to_string(),
Some(creds.refresh.clone()),
Some(creds.expires),
));
}
}
// Legacy: try the old PiAuth struct format (in case auth.json has optional fields)
let auth_path = crate::config::resolve_read_path("auth.json");
if auth_path.exists() {
if let Ok(content) = std::fs::read_to_string(&auth_path) {
if let Ok(auth) = serde_json::from_str::<PiAuth>(&content) {
let creds = &auth.anthropic;
if let (true, Some(access)) = (creds.auth_type == "oauth", creds.access.as_ref()) {
return Ok((
access.clone(),
"oauth".to_string(),
creds.refresh.clone(),
creds.expires,
));
}
}
}
}
// Fall back to env var
if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
return Ok((api_key, "api_key".to_string(), None, None));
}
// No Anthropic credentials — allow startup anyway for non-Anthropic providers.
// Auth will fail lazily on the first actual Anthropic API call.
Ok(("".to_string(), "none".to_string(), None, None))
}
}