claude_agent/auth/
config.rs

1//! OAuth configuration and request building for Claude Code CLI authentication.
2
3use std::collections::HashMap;
4
5use crate::client::BetaConfig;
6
7pub const DEFAULT_SYSTEM_PROMPT: &str = "You are Claude Code, Anthropic's official CLI for Claude.";
8pub const DEFAULT_USER_AGENT: &str = "claude-cli/2.0.76 (external, cli)";
9pub const DEFAULT_APP_IDENTIFIER: &str = "cli";
10pub const CLAUDE_CODE_BETA: &str = "claude-code-20250219";
11
12#[derive(Debug, Clone)]
13pub struct OAuthConfig {
14    pub system_prompt: String,
15    pub user_agent: String,
16    pub app_identifier: String,
17    pub url_params: HashMap<String, String>,
18    pub extra_headers: HashMap<String, String>,
19}
20
21impl Default for OAuthConfig {
22    fn default() -> Self {
23        Self {
24            system_prompt: DEFAULT_SYSTEM_PROMPT.to_string(),
25            user_agent: DEFAULT_USER_AGENT.to_string(),
26            app_identifier: DEFAULT_APP_IDENTIFIER.to_string(),
27            url_params: [("beta".to_string(), "true".to_string())]
28                .into_iter()
29                .collect(),
30            extra_headers: [(
31                "anthropic-dangerous-direct-browser-access".to_string(),
32                "true".to_string(),
33            )]
34            .into_iter()
35            .collect(),
36        }
37    }
38}
39
40impl OAuthConfig {
41    pub fn from_env() -> Self {
42        let mut config = Self::default();
43
44        if let Ok(prompt) = std::env::var("CLAUDE_AGENT_SYSTEM_PROMPT") {
45            config.system_prompt = prompt;
46        }
47        if let Ok(ua) = std::env::var("CLAUDE_AGENT_USER_AGENT") {
48            config.user_agent = ua;
49        }
50        if let Ok(app) = std::env::var("CLAUDE_AGENT_APP_IDENTIFIER") {
51            config.app_identifier = app;
52        }
53
54        config
55    }
56
57    pub fn builder() -> OAuthConfigBuilder {
58        OAuthConfigBuilder::default()
59    }
60
61    pub fn build_beta_header(&self, base: &BetaConfig) -> String {
62        let mut beta = base.clone();
63        beta.add(crate::client::BetaFeature::OAuth);
64        beta.add_custom(CLAUDE_CODE_BETA);
65        beta.header_value().unwrap_or_default()
66    }
67
68    pub fn build_url(&self, base_url: &str, endpoint: &str) -> String {
69        let url = format!("{}{}", base_url, endpoint);
70        if self.url_params.is_empty() {
71            url
72        } else {
73            let params: Vec<String> = self
74                .url_params
75                .iter()
76                .map(|(k, v)| format!("{}={}", k, v))
77                .collect();
78            format!("{}?{}", url, params.join("&"))
79        }
80    }
81
82    pub fn apply_headers(
83        &self,
84        req: reqwest::RequestBuilder,
85        token: &str,
86        api_version: &str,
87        beta: &BetaConfig,
88    ) -> reqwest::RequestBuilder {
89        let mut r = req
90            .header("Authorization", format!("Bearer {}", token))
91            .header("anthropic-version", api_version)
92            .header("content-type", "application/json")
93            .header("user-agent", &self.user_agent)
94            .header("x-app", &self.app_identifier);
95
96        for (k, v) in &self.extra_headers {
97            r = r.header(k.as_str(), v.as_str());
98        }
99
100        let beta_header = self.build_beta_header(beta);
101        if !beta_header.is_empty() {
102            r = r.header("anthropic-beta", beta_header);
103        }
104
105        r
106    }
107}
108
109pub struct OAuthConfigBuilder {
110    config: OAuthConfig,
111}
112
113impl Default for OAuthConfigBuilder {
114    fn default() -> Self {
115        Self {
116            config: OAuthConfig::from_env(),
117        }
118    }
119}
120
121impl OAuthConfigBuilder {
122    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
123        self.config.system_prompt = prompt.into();
124        self
125    }
126
127    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
128        self.config.user_agent = ua.into();
129        self
130    }
131
132    pub fn app_identifier(mut self, app: impl Into<String>) -> Self {
133        self.config.app_identifier = app.into();
134        self
135    }
136
137    pub fn url_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
138        self.config.url_params.insert(key.into(), value.into());
139        self
140    }
141
142    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
143        self.config.extra_headers.insert(key.into(), value.into());
144        self
145    }
146
147    pub fn build(self) -> OAuthConfig {
148        self.config
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_default_config() {
158        let config = OAuthConfig::default();
159        assert_eq!(config.system_prompt, DEFAULT_SYSTEM_PROMPT);
160        assert_eq!(config.user_agent, DEFAULT_USER_AGENT);
161        assert_eq!(config.app_identifier, DEFAULT_APP_IDENTIFIER);
162    }
163
164    #[test]
165    fn test_builder() {
166        let config = OAuthConfig::builder()
167            .system_prompt("Custom prompt")
168            .user_agent("my-app/1.0")
169            .build();
170
171        assert_eq!(config.system_prompt, "Custom prompt");
172        assert_eq!(config.user_agent, "my-app/1.0");
173    }
174
175    #[test]
176    fn test_url_params() {
177        let config = OAuthConfig::default();
178        assert_eq!(config.url_params.get("beta"), Some(&"true".to_string()));
179    }
180}