1use crate::config::{AuthStore, Credential, ProviderConfig};
5use crate::ProviderDeclaration as ProviderManifest;
6use crate::ValueSource;
7
8#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
9pub enum CredentialSource {
10 Explicit,
12 StoredApiKey,
13 StoredOauth,
14 ModelsJson,
15 Environment(String),
16 Headers,
17 Anonymous,
18 Missing,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
23#[serde(rename_all = "camelCase")]
24pub enum AuthStatus {
25 Missing,
26 Ready,
27 Expired,
28 Anonymous,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
33pub struct AuthState {
34 pub status: AuthStatus,
35 pub source: CredentialSource,
36}
37
38impl AuthState {
39 pub fn is_ready(&self) -> bool {
40 matches!(self.status, AuthStatus::Ready | AuthStatus::Anonymous)
41 }
42}
43
44pub(crate) fn state(
45 provider: &str,
46 config: Option<&ProviderConfig>,
47 store: &AuthStore,
48 manifest: Option<&ProviderManifest>,
49 values: &dyn ValueSource,
50 refreshable: bool,
51 has_headers: impl FnOnce() -> bool,
52) -> AuthState {
53 let credential = resolve_refreshable(provider, config, store, manifest, values, refreshable);
54 let source = if credential.source.is_present() {
55 credential.source
56 } else if has_headers() {
57 CredentialSource::Headers
58 } else {
59 CredentialSource::Missing
60 };
61 let status = match source {
62 CredentialSource::Anonymous => AuthStatus::Anonymous,
63 CredentialSource::Missing => match store.get(provider) {
64 Some(Credential::Oauth { expires, .. })
65 if *expires != 0 && *expires <= values.now_unix_seconds() =>
66 {
67 AuthStatus::Expired
68 }
69 _ => AuthStatus::Missing,
70 },
71 _ => AuthStatus::Ready,
72 };
73 AuthState { status, source }
74}
75
76impl CredentialSource {
77 pub fn is_present(&self) -> bool {
78 !matches!(self, Self::Missing)
79 }
80
81 pub fn as_json_str(&self) -> &str {
82 match self {
83 Self::Explicit => "override",
84 Self::StoredApiKey => "auth.json",
85 Self::StoredOauth => "oauth",
86 Self::ModelsJson => "models.json",
87 Self::Environment(name) => name,
88 Self::Headers => "headers",
89 Self::Anonymous => "anonymous",
90 Self::Missing => "none",
91 }
92 }
93}
94
95pub const OAUTH_REFRESH_MARGIN_SECS: i64 = 60;
98
99pub fn oauth_needs_refresh(expires: i64, now: i64) -> bool {
102 expires != 0 && expires <= now.saturating_add(OAUTH_REFRESH_MARGIN_SECS)
103}
104
105pub struct RefreshedOAuth {
108 pub access: String,
109 pub expires: i64,
111}
112
113pub trait OAuthRefresher: Send + Sync {
117 fn supports(&self, provider: &str) -> bool;
119 fn refresh(&self, provider: &str) -> Result<RefreshedOAuth, String>;
123}
124
125pub struct ResolvedCredential {
127 pub key: Option<String>,
128 pub source: CredentialSource,
129 pub bearer: bool,
130}
131
132pub fn builtin_api_key_env(provider: &str) -> Option<&'static str> {
135 match provider {
136 "anthropic" => Some("ANTHROPIC_API_KEY"),
137 "openai" => Some("OPENAI_API_KEY"),
138 _ => None,
139 }
140}
141
142fn nonempty(value: Option<String>) -> Option<String> {
143 value.filter(|key| !key.trim().is_empty())
144}
145
146pub fn resolve(
147 provider: &str,
148 config: Option<&ProviderConfig>,
149 store: &AuthStore,
150 manifest: Option<&ProviderManifest>,
151 values: &dyn ValueSource,
152) -> ResolvedCredential {
153 resolve_refreshable(provider, config, store, manifest, values, false)
154}
155
156pub(crate) fn resolve_refreshable(
159 provider: &str,
160 config: Option<&ProviderConfig>,
161 store: &AuthStore,
162 manifest: Option<&ProviderManifest>,
163 values: &dyn ValueSource,
164 refreshable: bool,
165) -> ResolvedCredential {
166 let bearer = config
167 .and_then(|config| config.auth_header)
168 .unwrap_or(false);
169 let credential = |key, source, bearer| ResolvedCredential {
170 key: Some(key),
171 source,
172 bearer,
173 };
174 match store.get(provider) {
175 Some(Credential::ApiKey {
176 key: Some(key),
177 env,
178 }) => {
179 if let Some(key) = nonempty(crate::values::resolve_config_value(
180 key,
181 env.as_ref(),
182 values,
183 true,
184 )) {
185 return credential(key, CredentialSource::StoredApiKey, bearer);
186 }
187 }
188 Some(Credential::Oauth {
189 access,
190 refresh,
191 expires,
192 }) if !access.trim().is_empty() => {
193 let now = values.now_unix_seconds();
194 if *expires == 0 || *expires > now || (refreshable && !refresh.trim().is_empty()) {
195 return credential(access.clone(), CredentialSource::StoredOauth, true);
196 }
197 }
198 _ => {}
199 }
200 if let Some(key) = config
201 .and_then(|config| config.api_key.as_deref())
202 .and_then(|raw| nonempty(crate::values::resolve_config_value(raw, None, values, true)))
203 {
204 return credential(key, CredentialSource::ModelsJson, bearer);
205 }
206 if manifest.is_none() && provider == "anthropic" {
208 if let Some(key) = nonempty(values.env("ANTHROPIC_AUTH_TOKEN")) {
209 return credential(
210 key,
211 CredentialSource::Environment("ANTHROPIC_AUTH_TOKEN".into()),
212 true,
213 );
214 }
215 }
216 let env = match manifest {
217 Some(manifest) => manifest.api_key_env.as_deref(),
218 None => builtin_api_key_env(provider),
219 };
220 if let Some(name) = env {
221 if let Some(key) = nonempty(values.env(name)) {
222 return credential(key, CredentialSource::Environment(name.into()), bearer);
223 }
224 }
225 ResolvedCredential {
226 key: None,
227 source: if manifest.is_some_and(|manifest| !manifest.requires_auth) {
228 CredentialSource::Anonymous
229 } else {
230 CredentialSource::Missing
231 },
232 bearer,
233 }
234}
235
236pub fn has_header_auth(headers: Option<&std::collections::BTreeMap<String, String>>) -> bool {
237 headers.is_some_and(|headers| {
238 headers.iter().any(|(name, value)| {
239 ["authorization", "x-api-key", "cf-aig-authorization"]
240 .contains(&name.to_ascii_lowercase().as_str())
241 && !value.trim().is_empty()
242 })
243 })
244}