1use once_cell::sync::Lazy;
5use std::env;
6
7pub const CLAUDE_AI_INFERENCE_SCOPE: &str = "user:inference";
8pub const CLAUDE_AI_PROFILE_SCOPE: &str = "user:profile";
9const CONSOLE_SCOPE: &str = "org:create_api_key";
10pub const OAUTH_BETA_HEADER: &str = "oauth-2025-04-20";
11
12pub const CONSOLE_OAUTH_SCOPES: &[&str] = &[CONSOLE_SCOPE, CLAUDE_AI_PROFILE_SCOPE];
13
14pub const CLAUDE_AI_OAUTH_SCOPES: &[&str] = &[
15 CLAUDE_AI_PROFILE_SCOPE,
16 CLAUDE_AI_INFERENCE_SCOPE,
17 "user:sessions:claude_code",
18 "user:mcp_servers",
19 "user:file_upload",
20];
21
22pub fn get_all_oauth_scopes() -> Vec<&'static str> {
23 let mut scopes: Vec<&str> = CONSOLE_OAUTH_SCOPES.to_vec();
24 for scope in CLAUDE_AI_OAUTH_SCOPES {
25 if !scopes.contains(scope) {
26 scopes.push(scope);
27 }
28 }
29 scopes
30}
31
32#[derive(Debug, Clone, Copy, PartialEq)]
33pub enum OauthConfigType {
34 Prod,
35 Staging,
36 Local,
37}
38
39fn get_oauth_config_type() -> OauthConfigType {
40 let user_type = env::var("USER_TYPE").unwrap_or_default();
41 if user_type == "ant" {
42 let use_local = env::var("USE_LOCAL_OAUTH")
43 .map(|v| v != "0" && v.to_lowercase() != "false")
44 .unwrap_or(false);
45 if use_local {
46 return OauthConfigType::Local;
47 }
48 let use_staging = env::var("USE_STAGING_OAUTH")
49 .map(|v| v != "0" && v.to_lowercase() != "false")
50 .unwrap_or(false);
51 if use_staging {
52 return OauthConfigType::Staging;
53 }
54 }
55 OauthConfigType::Prod
56}
57
58pub fn file_suffix_for_oauth_config() -> String {
59 if env::var("AI_CODE_CUSTOM_OAUTH_URL").is_ok() {
60 return "-custom-oauth".to_string();
61 }
62 match get_oauth_config_type() {
63 OauthConfigType::Local => "-local-oauth".to_string(),
64 OauthConfigType::Staging => "-staging-oauth".to_string(),
65 OauthConfigType::Prod => "".to_string(),
66 }
67}
68
69#[derive(Debug, Clone)]
70pub struct OauthConfig {
71 pub base_api_url: String,
72 pub console_authorize_url: String,
73 pub claude_ai_authorize_url: String,
74 pub claude_ai_origin: String,
75 pub token_url: String,
76 pub api_key_url: String,
77 pub roles_url: String,
78 pub console_success_url: String,
79 pub claudeai_success_url: String,
80 pub manual_redirect_url: String,
81 pub client_id: String,
82 pub oauth_file_suffix: String,
83 pub mcp_proxy_url: String,
84 pub mcp_proxy_path: String,
85}
86
87pub const MCP_CLIENT_METADATA_URL: &str = "https://claude.ai/oauth/claude-code-client-metadata";
88
89const ALLOWED_OAUTH_BASE_URLS: &[&str] = &[
90 "https://beacon.claude-ai.staging.ant.dev",
91 "https://claude.fedstart.com",
92 "https://claude-staging.fedstart.com",
93];
94
95fn get_local_oauth_config() -> OauthConfig {
96 let api = env::var("CLAUDE_LOCAL_OAUTH_API_BASE")
97 .map(|s| s.trim_end_matches('/').to_string())
98 .unwrap_or_else(|_| "http://localhost:8000".to_string());
99 let apps = env::var("CLAUDE_LOCAL_OAUTH_APPS_BASE")
100 .map(|s| s.trim_end_matches('/').to_string())
101 .unwrap_or_else(|_| "http://localhost:4000".to_string());
102 let console_base = env::var("CLAUDE_LOCAL_OAUTH_CONSOLE_BASE")
103 .map(|s| s.trim_end_matches('/').to_string())
104 .unwrap_or_else(|_| "http://localhost:3000".to_string());
105
106 OauthConfig {
107 base_api_url: api.clone(),
108 console_authorize_url: format!("{}/oauth/authorize", console_base),
109 claude_ai_authorize_url: format!("{}/oauth/authorize", apps),
110 claude_ai_origin: apps,
111 token_url: format!("{}/v1/oauth/token", api),
112 api_key_url: format!("{}/api/oauth/claude_cli/create_api_key", api),
113 roles_url: format!("{}/api/oauth/claude_cli/roles", api),
114 console_success_url: format!(
115 "{}/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code",
116 console_base
117 ),
118 claudeai_success_url: format!("{}/oauth/code/success?app=claude-code", console_base),
119 manual_redirect_url: format!("{}/oauth/code/callback", console_base),
120 client_id: "22422756-60c9-4084-8eb7-27705fd5cf9a".to_string(),
121 oauth_file_suffix: "-local-oauth".to_string(),
122 mcp_proxy_url: "http://localhost:8205".to_string(),
123 mcp_proxy_path: "/v1/toolbox/shttp/mcp/{server_id}".to_string(),
124 }
125}
126
127static PROD_OAUTH_CONFIG: Lazy<OauthConfig> = Lazy::new(|| OauthConfig {
128 base_api_url: "https://api.anthropic.com".to_string(),
129 console_authorize_url: "https://platform.claude.com/oauth/authorize".to_string(),
130 claude_ai_authorize_url: "https://claude.com/cai/oauth/authorize".to_string(),
131 claude_ai_origin: "https://claude.ai".to_string(),
132 token_url: "https://platform.claude.com/v1/oauth/token".to_string(),
133 api_key_url: "https://api.anthropic.com/api/oauth/claude_cli/create_api_key".to_string(),
134 roles_url: "https://api.anthropic.com/api/oauth/claude_cli/roles".to_string(),
135 console_success_url:
136 "https://platform.claude.com/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code"
137 .to_string(),
138 claudeai_success_url: "https://platform.claude.com/oauth/code/success?app=claude-code"
139 .to_string(),
140 manual_redirect_url: "https://platform.claude.com/oauth/code/callback".to_string(),
141 client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e".to_string(),
142 oauth_file_suffix: "".to_string(),
143 mcp_proxy_url: "https://mcp-proxy.anthropic.com".to_string(),
144 mcp_proxy_path: "/v1/mcp/{server_id}".to_string(),
145});
146
147pub fn get_oauth_config() -> OauthConfig {
148 let base_config = match get_oauth_config_type() {
149 OauthConfigType::Local => get_local_oauth_config(),
150 OauthConfigType::Staging => {
151 if env::var("USER_TYPE").map(|t| t == "ant").unwrap_or(false) {
153 OauthConfig {
154 base_api_url: "https://api-staging.anthropic.com".to_string(),
155 console_authorize_url:
156 "https://platform.staging.ant.dev/oauth/authorize".to_string(),
157 claude_ai_authorize_url:
158 "https://claude-ai.staging.ant.dev/oauth/authorize".to_string(),
159 claude_ai_origin: "https://claude-ai.staging.ant.dev".to_string(),
160 token_url: "https://platform.staging.ant.dev/v1/oauth/token".to_string(),
161 api_key_url:
162 "https://api-staging.anthropic.com/api/oauth/claude_cli/create_api_key"
163 .to_string(),
164 roles_url:
165 "https://api-staging.anthropic.com/api/oauth/claude_cli/roles".to_string(),
166 console_success_url:
167 "https://platform.staging.ant.dev/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code"
168 .to_string(),
169 claudeai_success_url:
170 "https://platform.staging.ant.dev/oauth/code/success?app=claude-code"
171 .to_string(),
172 manual_redirect_url:
173 "https://platform.staging.ant.dev/oauth/code/callback".to_string(),
174 client_id: "22422756-60c9-4084-8eb7-27705fd5cf9a".to_string(),
175 oauth_file_suffix: "-staging-oauth".to_string(),
176 mcp_proxy_url: "https://mcp-proxy-staging.anthropic.com".to_string(),
177 mcp_proxy_path: "/v1/mcp/{server_id}".to_string(),
178 }
179 } else {
180 PROD_OAUTH_CONFIG.clone()
181 }
182 }
183 OauthConfigType::Prod => PROD_OAUTH_CONFIG.clone(),
184 };
185
186 let mut config = base_config;
187
188 if let Ok(oauth_base_url) = env::var("AI_CODE_CUSTOM_OAUTH_URL") {
190 let base = oauth_base_url.trim_end_matches('/').to_string();
191 if !ALLOWED_OAUTH_BASE_URLS.contains(&base.as_str()) {
192 panic!("AI_CODE_CUSTOM_OAUTH_URL is not an approved endpoint.");
193 }
194 config.base_api_url = base.clone();
195 config.console_authorize_url = format!("{}/oauth/authorize", base);
196 config.claude_ai_authorize_url = format!("{}/oauth/authorize", base);
197 config.claude_ai_origin = base.clone();
198 config.token_url = format!("{}/v1/oauth/token", base);
199 config.api_key_url = format!("{}/api/oauth/claude_cli/create_api_key", base);
200 config.roles_url = format!("{}/api/oauth/claude_cli/roles", base);
201 config.console_success_url = format!("{}/oauth/code/success?app=claude-code", base);
202 config.claudeai_success_url = format!("{}/oauth/code/success?app=claude-code", base);
203 config.manual_redirect_url = format!("{}/oauth/code/callback", base);
204 config.oauth_file_suffix = "-custom-oauth".to_string();
205 }
206
207 if let Ok(client_id_override) = env::var("AI_CODE_OAUTH_CLIENT_ID") {
209 config.client_id = client_id_override;
210 }
211
212 config
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_get_all_oauth_scopes() {
221 let scopes = get_all_oauth_scopes();
222 assert!(scopes.contains(&CONSOLE_SCOPE));
223 assert!(scopes.contains(&CLAUDE_AI_INFERENCE_SCOPE));
224 }
225
226 #[test]
227 fn test_file_suffix_for_oauth_config() {
228 let _ = file_suffix_for_oauth_config();
229 }
230}