anthropic_auth/
types.rs

1use serde::{Deserialize, Serialize};
2use std::time::{Duration, SystemTime, UNIX_EPOCH};
3
4/// OAuth mode for Anthropic authentication
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum OAuthMode {
7    /// Claude Pro/Max subscription (uses claude.ai)
8    Max,
9    /// API key creation (uses console.anthropic.com)
10    Console,
11}
12
13/// OAuth token set containing access token, refresh token, and expiration info
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TokenSet {
16    /// The access token used to authenticate API requests
17    pub access_token: String,
18    /// The refresh token used to obtain new access tokens
19    pub refresh_token: String,
20    /// Unix timestamp (seconds) when the access token expires
21    pub expires_at: u64,
22}
23
24impl TokenSet {
25    /// Check if the token is expired or will expire soon (within 5 minutes)
26    ///
27    /// This includes a 5-minute buffer to prevent race conditions where a token
28    /// expires between checking and using it.
29    pub fn is_expired(&self) -> bool {
30        self.expires_in() <= Duration::from_secs(300)
31    }
32
33    /// Get the duration until the token expires
34    ///
35    /// Returns `Duration::ZERO` if the token is already expired.
36    pub fn expires_in(&self) -> Duration {
37        let now = SystemTime::now()
38            .duration_since(UNIX_EPOCH)
39            .unwrap()
40            .as_secs();
41
42        if self.expires_at > now {
43            Duration::from_secs(self.expires_at - now)
44        } else {
45            Duration::ZERO
46        }
47    }
48
49    /// Validate the token structure
50    ///
51    /// Checks that the token fields are non-empty and properly formatted.
52    pub fn validate(&self) -> Result<(), &'static str> {
53        if self.access_token.is_empty() {
54            return Err("access_token is empty");
55        }
56        if self.refresh_token.is_empty() {
57            return Err("refresh_token is empty");
58        }
59        if self.expires_at == 0 {
60            return Err("expires_at is invalid");
61        }
62        // Check if expires_at is reasonable (not too far in past or future)
63        let now = SystemTime::now()
64            .duration_since(UNIX_EPOCH)
65            .unwrap()
66            .as_secs();
67        // Token shouldn't be more than 1 year in the future
68        if self.expires_at > now + 31536000 {
69            return Err("expires_at is too far in the future");
70        }
71        Ok(())
72    }
73}
74
75/// OAuth authorization flow information
76///
77/// Contains the authorization URL, PKCE verifier, and state token needed to complete
78/// the OAuth flow.
79#[derive(Debug, Clone)]
80pub struct OAuthFlow {
81    /// The URL the user should visit to authorize the application
82    pub authorization_url: String,
83    /// The PKCE verifier used to exchange the authorization code for tokens
84    pub verifier: String,
85    /// The state token for CSRF protection
86    pub state: String,
87    /// The OAuth mode (Max or Console)
88    pub mode: OAuthMode,
89}
90
91/// Configuration for the Anthropic OAuth client
92#[derive(Debug, Clone)]
93pub struct OAuthConfig {
94    /// OAuth client ID (default: "9d1c250a-e61b-44d9-88ed-5944d1962f5e")
95    pub client_id: String,
96    /// Redirect URI for OAuth callback (default: "http://localhost:1455/callback")
97    pub redirect_uri: String,
98}
99
100impl Default for OAuthConfig {
101    fn default() -> Self {
102        Self {
103            client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e".to_string(),
104            redirect_uri: "http://localhost:1455/callback".to_string(),
105        }
106    }
107}
108
109impl OAuthConfig {
110    /// Create a new config builder
111    pub fn builder() -> OAuthConfigBuilder {
112        OAuthConfigBuilder::default()
113    }
114}
115
116/// Builder for OAuthConfig
117#[derive(Debug, Clone, Default)]
118pub struct OAuthConfigBuilder {
119    client_id: Option<String>,
120    redirect_uri: Option<String>,
121}
122
123impl OAuthConfigBuilder {
124    /// Set the OAuth client ID
125    pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
126        self.client_id = Some(client_id.into());
127        self
128    }
129
130    /// Set the redirect URI
131    pub fn redirect_uri(mut self, redirect_uri: impl Into<String>) -> Self {
132        self.redirect_uri = Some(redirect_uri.into());
133        self
134    }
135
136    /// Set the redirect URI with a custom port
137    pub fn redirect_port(mut self, port: u16) -> Self {
138        self.redirect_uri = Some(format!("http://localhost:{}/callback", port));
139        self
140    }
141
142    /// Build the OAuthConfig
143    pub fn build(self) -> OAuthConfig {
144        let defaults = OAuthConfig::default();
145        OAuthConfig {
146            client_id: self.client_id.unwrap_or(defaults.client_id),
147            redirect_uri: self.redirect_uri.unwrap_or(defaults.redirect_uri),
148        }
149    }
150}
151
152/// Token response from OAuth server
153#[derive(Debug, Deserialize)]
154pub(crate) struct TokenResponse {
155    pub access_token: String,
156    pub refresh_token: Option<String>,
157    pub expires_in: Option<u64>,
158}
159
160impl From<TokenResponse> for TokenSet {
161    fn from(response: TokenResponse) -> Self {
162        let expires_at = SystemTime::now()
163            .duration_since(UNIX_EPOCH)
164            .unwrap()
165            .as_secs()
166            + response.expires_in.unwrap_or(3600);
167
168        TokenSet {
169            access_token: response.access_token,
170            refresh_token: response.refresh_token.unwrap_or_default(),
171            expires_at,
172        }
173    }
174}
175
176/// API key creation response
177#[derive(Debug, Deserialize)]
178pub(crate) struct ApiKeyResponse {
179    pub raw_key: String,
180}