use crate::commands::cli_session::fetch_cloudflare_credentials_from_dashboard;
use crate::commands::cloudflare_config::{
ensure_cloudflare_credentials_interactive, is_interactive_terminal,
};
use crate::config::{
cloudflare_api_token_candidates, cloudflare_api_token_candidates_builds_first,
resolve_cloudflare_account_id, resolve_cloudflare_api_token,
};
use xbp_providers::cloudflare::CloudflareClient;
#[derive(Debug, Clone, Default)]
pub struct CloudflareCredentialOverrides {
pub token: Option<String>,
pub account_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ResolvedCloudflareCredentials {
pub token: String,
pub account_id: String,
pub token_source: String,
}
pub async fn resolve_cloudflare_credentials(
overrides: CloudflareCredentialOverrides,
require_account_id: bool,
) -> Result<(String, String), String> {
let resolved = resolve_cloudflare_credentials_detailed(overrides, require_account_id).await?;
Ok((resolved.token, resolved.account_id))
}
pub async fn resolve_cloudflare_credentials_detailed(
overrides: CloudflareCredentialOverrides,
require_account_id: bool,
) -> Result<ResolvedCloudflareCredentials, String> {
match try_resolve_cloudflare_credentials_detailed(&overrides, require_account_id).await {
Ok(credentials) => Ok(credentials),
Err(error) if is_interactive_terminal() && overrides_are_empty(&overrides) => {
ensure_cloudflare_credentials_interactive().await?;
try_resolve_cloudflare_credentials_detailed(&overrides, require_account_id)
.await
.map_err(|_| error)
}
Err(error) => Err(error),
}
}
pub async fn build_cloudflare_client(
overrides: CloudflareCredentialOverrides,
require_account_id: bool,
) -> Result<CloudflareClient, String> {
let (token, account_id) = resolve_cloudflare_credentials(overrides, require_account_id).await?;
CloudflareClient::new(token, account_id)
}
pub async fn cloudflare_credential_try_list(
overrides: CloudflareCredentialOverrides,
require_account_id: bool,
) -> Result<Vec<ResolvedCloudflareCredentials>, String> {
cloudflare_credential_try_list_inner(overrides, require_account_id, false).await
}
pub async fn cloudflare_credential_try_list_builds_first(
overrides: CloudflareCredentialOverrides,
require_account_id: bool,
) -> Result<Vec<ResolvedCloudflareCredentials>, String> {
cloudflare_credential_try_list_inner(overrides, require_account_id, true).await
}
async fn cloudflare_credential_try_list_inner(
overrides: CloudflareCredentialOverrides,
require_account_id: bool,
prefer_user_for_builds: bool,
) -> Result<Vec<ResolvedCloudflareCredentials>, String> {
let account_id_override = normalize_override(overrides.account_id.clone());
let account_id = resolve_account_id(account_id_override.clone(), require_account_id)?;
let mut out = Vec::new();
let mut seen = std::collections::BTreeSet::new();
if let Some(token) = normalize_override(overrides.token.clone()) {
if seen.insert(token.clone()) {
out.push(ResolvedCloudflareCredentials {
token,
account_id: account_id.clone(),
token_source: "CLI --token override".into(),
});
}
}
let candidates = if prefer_user_for_builds {
cloudflare_api_token_candidates_builds_first()
} else {
cloudflare_api_token_candidates()
};
for (source, token) in candidates {
if seen.insert(token.clone()) {
out.push(ResolvedCloudflareCredentials {
token,
account_id: account_id.clone(),
token_source: source,
});
}
}
if let Some(credentials) = fetch_cloudflare_credentials_from_dashboard().await? {
let token = credentials.access_token.trim().to_string();
if !token.is_empty() && seen.insert(token.clone()) {
let acc = if account_id.is_empty() {
credentials.account_id
} else {
account_id.clone()
};
out.push(ResolvedCloudflareCredentials {
token,
account_id: acc,
token_source: "dashboard-linked (xbp login / cloudflare login)".into(),
});
}
}
if out.is_empty() {
return Err(missing_cloudflare_credentials_message());
}
Ok(out)
}
async fn try_resolve_cloudflare_credentials_detailed(
overrides: &CloudflareCredentialOverrides,
require_account_id: bool,
) -> Result<ResolvedCloudflareCredentials, String> {
let token_override = normalize_override(overrides.token.clone());
let account_id_override = normalize_override(overrides.account_id.clone());
if let Some(token) = token_override {
let account_id = resolve_account_id(account_id_override, require_account_id)?;
return Ok(ResolvedCloudflareCredentials {
token,
account_id,
token_source: "CLI --token override".into(),
});
}
if let Some((source, token)) = cloudflare_api_token_candidates().into_iter().next() {
let account_id = resolve_account_id(account_id_override, require_account_id)?;
return Ok(ResolvedCloudflareCredentials {
token,
account_id,
token_source: source,
});
}
if let Some(token) = resolve_cloudflare_api_token() {
let account_id = resolve_account_id(account_id_override, require_account_id)?;
return Ok(ResolvedCloudflareCredentials {
token,
account_id,
token_source: "resolved Cloudflare token".into(),
});
}
if let Some(credentials) = fetch_cloudflare_credentials_from_dashboard().await? {
let account_id = account_id_override.unwrap_or(credentials.account_id);
return Ok(ResolvedCloudflareCredentials {
token: credentials.access_token,
account_id,
token_source: "dashboard-linked (xbp login / cloudflare login)".into(),
});
}
Err(missing_cloudflare_credentials_message())
}
fn resolve_account_id(
account_id_override: Option<String>,
require_account_id: bool,
) -> Result<String, String> {
if let Some(account_id) = account_id_override.or_else(resolve_cloudflare_account_id) {
return Ok(account_id);
}
if require_account_id {
return Err(missing_cloudflare_account_id_message());
}
Ok(String::new())
}
fn overrides_are_empty(overrides: &CloudflareCredentialOverrides) -> bool {
overrides.token.is_none() && overrides.account_id.is_none()
}
fn normalize_override(value: Option<String>) -> Option<String> {
value
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
fn missing_cloudflare_credentials_message() -> String {
"Cloudflare credentials are not configured.".to_string()
}
fn missing_cloudflare_account_id_message() -> String {
"Cloudflare account ID is not configured.".to_string()
}
#[cfg(test)]
mod tests {
use super::{normalize_override, overrides_are_empty, CloudflareCredentialOverrides};
#[test]
fn normalize_override_trims_and_rejects_empty_values() {
assert_eq!(
normalize_override(Some(" token ".to_string())),
Some("token".to_string())
);
assert_eq!(normalize_override(Some(" ".to_string())), None);
assert_eq!(normalize_override(None), None);
}
#[test]
fn detects_empty_overrides() {
assert!(overrides_are_empty(
&CloudflareCredentialOverrides::default()
));
assert!(!overrides_are_empty(&CloudflareCredentialOverrides {
token: Some("token".to_string()),
account_id: None,
}));
}
}