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 has_headers: impl FnOnce() -> bool,
51) -> AuthState {
52 let credential = resolve(provider, config, store, manifest, values);
53 let source = if credential.source.is_present() {
54 credential.source
55 } else if has_headers() {
56 CredentialSource::Headers
57 } else {
58 CredentialSource::Missing
59 };
60 let status = match source {
61 CredentialSource::Anonymous => AuthStatus::Anonymous,
62 CredentialSource::Missing => match store.get(provider) {
63 Some(Credential::Oauth { expires, .. })
64 if *expires != 0 && *expires <= values.now_unix_seconds() =>
65 {
66 AuthStatus::Expired
67 }
68 _ => AuthStatus::Missing,
69 },
70 _ => AuthStatus::Ready,
71 };
72 AuthState { status, source }
73}
74
75impl CredentialSource {
76 pub fn is_present(&self) -> bool {
77 !matches!(self, Self::Missing)
78 }
79
80 pub fn as_json_str(&self) -> &str {
81 match self {
82 Self::Explicit => "override",
83 Self::StoredApiKey => "auth.json",
84 Self::StoredOauth => "oauth",
85 Self::ModelsJson => "models.json",
86 Self::Environment(name) => name,
87 Self::Headers => "headers",
88 Self::Anonymous => "anonymous",
89 Self::Missing => "none",
90 }
91 }
92}
93
94pub struct ResolvedCredential {
96 pub key: Option<String>,
97 pub source: CredentialSource,
98 pub bearer: bool,
99}
100
101pub fn builtin_api_key_env(provider: &str) -> Option<&'static str> {
104 match provider {
105 "anthropic" => Some("ANTHROPIC_API_KEY"),
106 "openai" => Some("OPENAI_API_KEY"),
107 _ => None,
108 }
109}
110
111fn nonempty(value: Option<String>) -> Option<String> {
112 value.filter(|key| !key.trim().is_empty())
113}
114
115pub fn resolve(
116 provider: &str,
117 config: Option<&ProviderConfig>,
118 store: &AuthStore,
119 manifest: Option<&ProviderManifest>,
120 values: &dyn ValueSource,
121) -> ResolvedCredential {
122 let bearer = config
123 .and_then(|config| config.auth_header)
124 .unwrap_or(false);
125 let credential = |key, source, bearer| ResolvedCredential {
126 key: Some(key),
127 source,
128 bearer,
129 };
130 match store.get(provider) {
131 Some(Credential::ApiKey {
132 key: Some(key),
133 env,
134 }) => {
135 if let Some(key) = nonempty(crate::values::resolve_config_value(
136 key,
137 env.as_ref(),
138 values,
139 true,
140 )) {
141 return credential(key, CredentialSource::StoredApiKey, bearer);
142 }
143 }
144 Some(Credential::Oauth {
145 access, expires, ..
146 }) if !access.trim().is_empty() => {
147 let now = values.now_unix_seconds();
148 if *expires == 0 || *expires > now {
149 return credential(access.clone(), CredentialSource::StoredOauth, true);
150 }
151 }
152 _ => {}
153 }
154 if let Some(key) = config
155 .and_then(|config| config.api_key.as_deref())
156 .and_then(|raw| nonempty(crate::values::resolve_config_value(raw, None, values, true)))
157 {
158 return credential(key, CredentialSource::ModelsJson, bearer);
159 }
160 if manifest.is_none() && provider == "anthropic" {
162 if let Some(key) = nonempty(values.env("ANTHROPIC_AUTH_TOKEN")) {
163 return credential(
164 key,
165 CredentialSource::Environment("ANTHROPIC_AUTH_TOKEN".into()),
166 true,
167 );
168 }
169 }
170 let env = match manifest {
171 Some(manifest) => manifest.api_key_env.as_deref(),
172 None => builtin_api_key_env(provider),
173 };
174 if let Some(name) = env {
175 if let Some(key) = nonempty(values.env(name)) {
176 return credential(key, CredentialSource::Environment(name.into()), bearer);
177 }
178 }
179 ResolvedCredential {
180 key: None,
181 source: if manifest.is_some_and(|manifest| !manifest.requires_auth) {
182 CredentialSource::Anonymous
183 } else {
184 CredentialSource::Missing
185 },
186 bearer,
187 }
188}
189
190pub fn has_header_auth(headers: Option<&std::collections::BTreeMap<String, String>>) -> bool {
191 headers.is_some_and(|headers| {
192 headers.iter().any(|(name, value)| {
193 ["authorization", "x-api-key", "cf-aig-authorization"]
194 .contains(&name.to_ascii_lowercase().as_str())
195 && !value.trim().is_empty()
196 })
197 })
198}