1use crate::constants::env::{ai, ai_code};
8use crate::utils::http::get_user_agent;
9use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fs;
13use std::path::PathBuf;
14
15pub fn get_bridge_token_override() -> Option<String> {
21 if std::env::var(ai::USER_TYPE).ok().as_deref() == Some("ant") {
22 std::env::var(ai::BRIDGE_OAUTH_TOKEN).ok()
23 } else {
24 None
25 }
26}
27
28pub fn get_bridge_base_url_override() -> Option<String> {
30 if std::env::var(ai::USER_TYPE).ok().as_deref() == Some("ant") {
31 std::env::var(ai::BRIDGE_BASE_URL).ok()
32 } else {
33 None
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct OAuthTokens {
40 #[serde(rename = "accessToken")]
41 pub access_token: String,
42 #[serde(rename = "refreshToken")]
43 pub refresh_token: Option<String>,
44 #[serde(rename = "expiresAt")]
45 pub expires_at: Option<String>,
46 pub scopes: Vec<String>,
47 #[serde(rename = "subscriptionType")]
48 pub subscription_type: Option<String>,
49 #[serde(rename = "rateLimitTier")]
50 pub rate_limit_tier: Option<String>,
51}
52
53pub fn get_claude_ai_oauth_tokens() -> Option<OAuthTokens> {
55 if let Ok(token) = std::env::var(ai::OAUTH_TOKEN) {
57 if !token.is_empty() {
58 return Some(OAuthTokens {
59 access_token: token,
60 refresh_token: None,
61 expires_at: None,
62 scopes: vec!["user:inference".to_string()],
63 subscription_type: None,
64 rate_limit_tier: None,
65 });
66 }
67 }
68
69 if let Ok(token) = std::env::var(ai_code::OAUTH_TOKEN) {
70 if !token.is_empty() {
71 return Some(OAuthTokens {
72 access_token: token,
73 refresh_token: None,
74 expires_at: None,
75 scopes: vec!["user:inference".to_string()],
76 subscription_type: None,
77 rate_limit_tier: None,
78 });
79 }
80 }
81
82 if let Some(home) = dirs::home_dir() {
84 let ai_oauth_path = home.join(".ai").join("oauth").join("tokens.json");
86 if let Ok(tokens) = read_oauth_tokens_from_path(&ai_oauth_path) {
87 return Some(tokens);
88 }
89
90 let claude_oauth_path = home.join(".ai").join("oauth").join("tokens.json");
92 if let Ok(tokens) = read_oauth_tokens_from_path(&claude_oauth_path) {
93 return Some(tokens);
94 }
95 }
96
97 None
98}
99
100fn read_oauth_tokens_from_path(path: &PathBuf) -> Result<OAuthTokens, Box<dyn std::error::Error>> {
101 let content = fs::read_to_string(path)?;
102 let tokens: OAuthTokens = serde_json::from_str(&content)?;
103 Ok(tokens)
104}
105
106pub fn get_bridge_access_token() -> Option<String> {
109 if let Some(token) = get_bridge_token_override() {
111 return Some(token);
112 }
113
114 get_claude_ai_oauth_tokens().map(|t| t.access_token)
116}
117
118pub fn get_bridge_base_url() -> String {
121 if let Some(url) = get_bridge_base_url_override() {
123 return url;
124 }
125
126 get_oauth_config().base_api_url
128}
129
130pub fn get_bridge_headers() -> HashMap<String, String> {
132 let mut headers = HashMap::new();
133
134 if let Some(token) = get_bridge_access_token() {
135 headers.insert("Authorization".to_string(), format!("Bearer {}", token));
136 }
137
138 headers.insert("Content-Type".to_string(), "application/json".to_string());
139 headers.insert("anthropic-version".to_string(), "2023-06-01".to_string());
140 headers.insert("User-Agent".to_string(), get_user_agent());
141
142 headers
143}
144
145pub const HISTORY_PAGE_SIZE: u32 = 100;
150
151pub type SDKMessage = serde_json::Value;
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct HistoryPage {
155 pub events: Vec<SDKMessage>,
156 #[serde(rename = "firstId")]
157 pub first_id: Option<String>,
158 #[serde(rename = "hasMore")]
159 pub has_more: bool,
160}
161
162#[derive(Debug, Deserialize)]
163struct SessionEventsResponse {
164 data: Vec<SDKMessage>,
165 #[serde(rename = "has_more")]
166 has_more: bool,
167 #[serde(rename = "first_id")]
168 first_id: Option<String>,
169 #[allow(dead_code)]
170 #[serde(rename = "last_id")]
171 last_id: Option<String>,
172}
173
174#[derive(Debug, Clone)]
175pub struct HistoryAuthCtx {
176 pub base_url: String,
177 pub headers: HashMap<String, String>,
178}
179
180pub struct OauthConfig {
181 pub base_api_url: String,
182}
183
184fn get_oauth_config() -> OauthConfig {
185 if std::env::var(ai::USER_TYPE).ok().as_deref() == Some("ant") {
186 if std::env::var(ai::USE_LOCAL_OAUTH)
187 .map(|v| v == "1" || v.to_lowercase() == "true")
188 .unwrap_or(false)
189 {
190 let api = std::env::var(ai::CLAUDE_LOCAL_OAUTH_API_BASE)
191 .unwrap_or_else(|_| "http://localhost:8000".to_string());
192 return OauthConfig {
193 base_api_url: api.trim_end_matches('/').to_string(),
194 };
195 }
196 if std::env::var(ai::USE_STAGING_OAUTH)
197 .map(|v| v == "1" || v.to_lowercase() == "true")
198 .unwrap_or(false)
199 {
200 return OauthConfig {
201 base_api_url: "https://api-staging.anthropic.com".to_string(),
202 };
203 }
204 }
205
206 if let Ok(custom_url) = std::env::var(ai_code::CUSTOM_OAUTH_URL) {
207 let base = custom_url.trim_end_matches('/').to_string();
208 return OauthConfig { base_api_url: base };
209 }
210
211 OauthConfig {
212 base_api_url: "https://api.anthropic.com".to_string(),
213 }
214}
215
216pub async fn prepare_api_request() -> Result<(String, String), crate::AgentError> {
217 let access_token = get_access_token()?;
218 let org_uuid = get_org_uuid()?;
219 Ok((access_token, org_uuid))
220}
221
222fn get_access_token() -> Result<String, crate::AgentError> {
223 if let Ok(token) = std::env::var(ai_code::ACCESS_TOKEN) {
224 if !token.is_empty() {
225 return Ok(token);
226 }
227 }
228
229 if let Some(home) = dirs::home_dir() {
230 let keychain_path = home.join(".ai").join("oauth").join("tokens.json");
231 if let Ok(content) = std::fs::read_to_string(&keychain_path) {
232 if let Ok(tokens) = serde_json::from_str::<serde_json::Value>(&content) {
233 if let Some(token) = tokens.get("accessToken").and_then(|t| t.as_str()) {
234 return Ok(token.to_string());
235 }
236 }
237 }
238 }
239
240 Err(crate::AgentError::Auth(
241 "Claude Code web sessions require authentication with a Claude.ai account. Please run /login to authenticate, or check your authentication status with /status.".to_string(),
242 ))
243}
244
245fn get_org_uuid() -> Result<String, crate::AgentError> {
246 if let Ok(org) = std::env::var(ai_code::ORG_UUID) {
247 if !org.is_empty() {
248 return Ok(org);
249 }
250 }
251
252 if let Some(home) = dirs::home_dir() {
253 let settings_path = home.join(".ai").join("settings.json");
254 if let Ok(content) = std::fs::read_to_string(&settings_path) {
255 if let Ok(settings) = serde_json::from_str::<serde_json::Value>(&content) {
256 if let Some(org) = settings.get("orgUUID").and_then(|o| o.as_str()) {
257 return Ok(org.to_string());
258 }
259 }
260 }
261 }
262
263 Err(crate::AgentError::Auth(
264 "Organization UUID not found. Please authenticate with Claude Code.".to_string(),
265 ))
266}
267
268pub fn get_oauth_headers(access_token: &str) -> HashMap<String, String> {
269 let mut headers = HashMap::new();
270 headers.insert(
271 "Authorization".to_string(),
272 format!("Bearer {}", access_token),
273 );
274 headers.insert("Content-Type".to_string(), "application/json".to_string());
275 headers.insert("anthropic-version".to_string(), "2023-06-01".to_string());
276 headers.insert("User-Agent".to_string(), get_user_agent());
277 headers
278}
279
280pub async fn create_history_auth_ctx(
281 session_id: &str,
282) -> Result<HistoryAuthCtx, crate::AgentError> {
283 let (access_token, org_uuid) = prepare_api_request().await?;
284 let oauth_config = get_oauth_config();
285
286 let mut headers = get_oauth_headers(&access_token);
287 headers.insert(
288 "anthropic-beta".to_string(),
289 "ccr-byoc-2025-07-29".to_string(),
290 );
291 headers.insert("x-organization-uuid".to_string(), org_uuid);
292
293 let base_url = format!(
294 "{}/v1/sessions/{}/events",
295 oauth_config.base_api_url, session_id
296 );
297
298 Ok(HistoryAuthCtx { base_url, headers })
299}
300
301async fn fetch_page(
302 ctx: &HistoryAuthCtx,
303 params: &HashMap<String, serde_json::Value>,
304 label: &str,
305) -> Result<Option<HistoryPage>, crate::AgentError> {
306 let client = reqwest::Client::new();
307
308 let mut query_params: Vec<(&str, String)> = Vec::new();
309 for (key, value) in params {
310 query_params.push((key.as_str(), value.to_string()));
311 }
312
313 let mut header_map = HeaderMap::new();
314 for (key, value) in &ctx.headers {
315 if let (Ok(name), Ok(val)) = (key.parse::<HeaderName>(), value.parse::<HeaderValue>()) {
316 header_map.insert(name, val);
317 }
318 }
319
320 let resp = client
321 .get(&ctx.base_url)
322 .headers(header_map)
323 .query(&query_params)
324 .timeout(std::time::Duration::from_secs(15))
325 .send()
326 .await;
327
328 match resp {
329 Ok(response) => {
330 if response.status() == reqwest::StatusCode::OK {
331 let data: SessionEventsResponse = response
332 .json()
333 .await
334 .map_err(|e| crate::AgentError::Http(e))?;
335
336 Ok(Some(HistoryPage {
337 events: data.data,
338 first_id: data.first_id,
339 has_more: data.has_more,
340 }))
341 } else {
342 log_for_debugging(&format!("[{}] HTTP {}", label, response.status()));
343 Ok(None)
344 }
345 }
346 Err(e) => {
347 log_for_debugging(&format!("[{}] error: {}", label, e));
348 Ok(None)
349 }
350 }
351}
352
353fn log_for_debugging(message: &str) {
354 log::debug!("{}", message);
355}
356
357pub async fn fetch_latest_events(
358 ctx: &HistoryAuthCtx,
359 limit: u32,
360) -> Result<Option<HistoryPage>, crate::AgentError> {
361 let mut params = HashMap::new();
362 params.insert("limit".to_string(), serde_json::json!(limit));
363 params.insert("anchor_to_latest".to_string(), serde_json::json!(true));
364
365 fetch_page(ctx, ¶ms, "fetchLatestEvents").await
366}
367
368pub async fn fetch_older_events(
369 ctx: &HistoryAuthCtx,
370 before_id: &str,
371 limit: u32,
372) -> Result<Option<HistoryPage>, crate::AgentError> {
373 let mut params = HashMap::new();
374 params.insert("limit".to_string(), serde_json::json!(limit));
375 params.insert("before_id".to_string(), serde_json::json!(before_id));
376
377 fetch_page(ctx, ¶ms, "fetchOlderEvents").await
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn test_get_oauth_headers() {
386 let token = "test_token";
387 let headers = get_oauth_headers(token);
388
389 assert_eq!(
390 headers.get("Authorization"),
391 Some(&"Bearer test_token".to_string())
392 );
393 assert_eq!(
394 headers.get("Content-Type"),
395 Some(&"application/json".to_string())
396 );
397 assert_eq!(
398 headers.get("anthropic-version"),
399 Some(&"2023-06-01".to_string())
400 );
401 }
402
403 #[test]
404 fn test_history_page_default() {
405 let page = HistoryPage {
406 events: vec![],
407 first_id: None,
408 has_more: false,
409 };
410
411 assert_eq!(page.events.len(), 0);
412 assert_eq!(page.first_id, None);
413 assert_eq!(page.has_more, false);
414 }
415}