use once_cell::sync::Lazy;
use std::env;
pub const CLAUDE_AI_INFERENCE_SCOPE: &str = "user:inference";
pub const CLAUDE_AI_PROFILE_SCOPE: &str = "user:profile";
const CONSOLE_SCOPE: &str = "org:create_api_key";
pub const OAUTH_BETA_HEADER: &str = "oauth-2025-04-20";
pub const CONSOLE_OAUTH_SCOPES: &[&str] = &[CONSOLE_SCOPE, CLAUDE_AI_PROFILE_SCOPE];
pub const CLAUDE_AI_OAUTH_SCOPES: &[&str] = &[
CLAUDE_AI_PROFILE_SCOPE,
CLAUDE_AI_INFERENCE_SCOPE,
"user:sessions:claude_code",
"user:mcp_servers",
"user:file_upload",
];
pub fn get_all_oauth_scopes() -> Vec<&'static str> {
let mut scopes: Vec<&str> = CONSOLE_OAUTH_SCOPES.to_vec();
for scope in CLAUDE_AI_OAUTH_SCOPES {
if !scopes.contains(scope) {
scopes.push(scope);
}
}
scopes
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum OauthConfigType {
Prod,
Staging,
Local,
}
fn get_oauth_config_type() -> OauthConfigType {
let user_type = env::var("USER_TYPE").unwrap_or_default();
if user_type == "ant" {
let use_local = env::var("USE_LOCAL_OAUTH")
.map(|v| v != "0" && v.to_lowercase() != "false")
.unwrap_or(false);
if use_local {
return OauthConfigType::Local;
}
let use_staging = env::var("USE_STAGING_OAUTH")
.map(|v| v != "0" && v.to_lowercase() != "false")
.unwrap_or(false);
if use_staging {
return OauthConfigType::Staging;
}
}
OauthConfigType::Prod
}
pub fn file_suffix_for_oauth_config() -> String {
if env::var("AI_CODE_CUSTOM_OAUTH_URL").is_ok() {
return "-custom-oauth".to_string();
}
match get_oauth_config_type() {
OauthConfigType::Local => "-local-oauth".to_string(),
OauthConfigType::Staging => "-staging-oauth".to_string(),
OauthConfigType::Prod => "".to_string(),
}
}
#[derive(Debug, Clone)]
pub struct OauthConfig {
pub base_api_url: String,
pub console_authorize_url: String,
pub claude_ai_authorize_url: String,
pub claude_ai_origin: String,
pub token_url: String,
pub api_key_url: String,
pub roles_url: String,
pub console_success_url: String,
pub claudeai_success_url: String,
pub manual_redirect_url: String,
pub client_id: String,
pub oauth_file_suffix: String,
pub mcp_proxy_url: String,
pub mcp_proxy_path: String,
}
pub const MCP_CLIENT_METADATA_URL: &str = "https://claude.ai/oauth/claude-code-client-metadata";
const ALLOWED_OAUTH_BASE_URLS: &[&str] = &[
"https://beacon.claude-ai.staging.ant.dev",
"https://claude.fedstart.com",
"https://claude-staging.fedstart.com",
];
fn get_local_oauth_config() -> OauthConfig {
let api = env::var("CLAUDE_LOCAL_OAUTH_API_BASE")
.map(|s| s.trim_end_matches('/').to_string())
.unwrap_or_else(|_| "http://localhost:8000".to_string());
let apps = env::var("CLAUDE_LOCAL_OAUTH_APPS_BASE")
.map(|s| s.trim_end_matches('/').to_string())
.unwrap_or_else(|_| "http://localhost:4000".to_string());
let console_base = env::var("CLAUDE_LOCAL_OAUTH_CONSOLE_BASE")
.map(|s| s.trim_end_matches('/').to_string())
.unwrap_or_else(|_| "http://localhost:3000".to_string());
OauthConfig {
base_api_url: api.clone(),
console_authorize_url: format!("{}/oauth/authorize", console_base),
claude_ai_authorize_url: format!("{}/oauth/authorize", apps),
claude_ai_origin: apps,
token_url: format!("{}/v1/oauth/token", api),
api_key_url: format!("{}/api/oauth/claude_cli/create_api_key", api),
roles_url: format!("{}/api/oauth/claude_cli/roles", api),
console_success_url: format!(
"{}/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code",
console_base
),
claudeai_success_url: format!("{}/oauth/code/success?app=claude-code", console_base),
manual_redirect_url: format!("{}/oauth/code/callback", console_base),
client_id: "22422756-60c9-4084-8eb7-27705fd5cf9a".to_string(),
oauth_file_suffix: "-local-oauth".to_string(),
mcp_proxy_url: "http://localhost:8205".to_string(),
mcp_proxy_path: "/v1/toolbox/shttp/mcp/{server_id}".to_string(),
}
}
static PROD_OAUTH_CONFIG: Lazy<OauthConfig> = Lazy::new(|| OauthConfig {
base_api_url: "https://api.anthropic.com".to_string(),
console_authorize_url: "https://platform.claude.com/oauth/authorize".to_string(),
claude_ai_authorize_url: "https://claude.com/cai/oauth/authorize".to_string(),
claude_ai_origin: "https://claude.ai".to_string(),
token_url: "https://platform.claude.com/v1/oauth/token".to_string(),
api_key_url: "https://api.anthropic.com/api/oauth/claude_cli/create_api_key".to_string(),
roles_url: "https://api.anthropic.com/api/oauth/claude_cli/roles".to_string(),
console_success_url:
"https://platform.claude.com/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code"
.to_string(),
claudeai_success_url: "https://platform.claude.com/oauth/code/success?app=claude-code"
.to_string(),
manual_redirect_url: "https://platform.claude.com/oauth/code/callback".to_string(),
client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e".to_string(),
oauth_file_suffix: "".to_string(),
mcp_proxy_url: "https://mcp-proxy.anthropic.com".to_string(),
mcp_proxy_path: "/v1/mcp/{server_id}".to_string(),
});
pub fn get_oauth_config() -> OauthConfig {
let base_config = match get_oauth_config_type() {
OauthConfigType::Local => get_local_oauth_config(),
OauthConfigType::Staging => {
if env::var("USER_TYPE").map(|t| t == "ant").unwrap_or(false) {
OauthConfig {
base_api_url: "https://api-staging.anthropic.com".to_string(),
console_authorize_url:
"https://platform.staging.ant.dev/oauth/authorize".to_string(),
claude_ai_authorize_url:
"https://claude-ai.staging.ant.dev/oauth/authorize".to_string(),
claude_ai_origin: "https://claude-ai.staging.ant.dev".to_string(),
token_url: "https://platform.staging.ant.dev/v1/oauth/token".to_string(),
api_key_url:
"https://api-staging.anthropic.com/api/oauth/claude_cli/create_api_key"
.to_string(),
roles_url:
"https://api-staging.anthropic.com/api/oauth/claude_cli/roles".to_string(),
console_success_url:
"https://platform.staging.ant.dev/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code"
.to_string(),
claudeai_success_url:
"https://platform.staging.ant.dev/oauth/code/success?app=claude-code"
.to_string(),
manual_redirect_url:
"https://platform.staging.ant.dev/oauth/code/callback".to_string(),
client_id: "22422756-60c9-4084-8eb7-27705fd5cf9a".to_string(),
oauth_file_suffix: "-staging-oauth".to_string(),
mcp_proxy_url: "https://mcp-proxy-staging.anthropic.com".to_string(),
mcp_proxy_path: "/v1/mcp/{server_id}".to_string(),
}
} else {
PROD_OAUTH_CONFIG.clone()
}
}
OauthConfigType::Prod => PROD_OAUTH_CONFIG.clone(),
};
let mut config = base_config;
if let Ok(oauth_base_url) = env::var("AI_CODE_CUSTOM_OAUTH_URL") {
let base = oauth_base_url.trim_end_matches('/').to_string();
if !ALLOWED_OAUTH_BASE_URLS.contains(&base.as_str()) {
panic!("AI_CODE_CUSTOM_OAUTH_URL is not an approved endpoint.");
}
config.base_api_url = base.clone();
config.console_authorize_url = format!("{}/oauth/authorize", base);
config.claude_ai_authorize_url = format!("{}/oauth/authorize", base);
config.claude_ai_origin = base.clone();
config.token_url = format!("{}/v1/oauth/token", base);
config.api_key_url = format!("{}/api/oauth/claude_cli/create_api_key", base);
config.roles_url = format!("{}/api/oauth/claude_cli/roles", base);
config.console_success_url = format!("{}/oauth/code/success?app=claude-code", base);
config.claudeai_success_url = format!("{}/oauth/code/success?app=claude-code", base);
config.manual_redirect_url = format!("{}/oauth/code/callback", base);
config.oauth_file_suffix = "-custom-oauth".to_string();
}
if let Ok(client_id_override) = env::var("AI_CODE_OAUTH_CLIENT_ID") {
config.client_id = client_id_override;
}
config
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_all_oauth_scopes() {
let scopes = get_all_oauth_scopes();
assert!(scopes.contains(&CONSOLE_SCOPE));
assert!(scopes.contains(&CLAUDE_AI_INFERENCE_SCOPE));
}
#[test]
fn test_file_suffix_for_oauth_config() {
let _ = file_suffix_for_oauth_config();
}
}