1use alloc::borrow::ToOwned;
4use alloc::string::String;
5use alloc::vec::Vec;
6use core::time::Duration;
7use std::time::SystemTime;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{Secret, lenient};
12
13pub const CLAUDE_CODE_CLIENT_ID: &str = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
16
17pub const DEFAULT_SCOPES: &[&str] = &[
20 "user:profile",
21 "user:inference",
22 "user:sessions:claude_code",
23 "user:mcp_servers",
24 "user:file_upload",
25 "user:plugins",
26];
27
28#[derive(Debug, Clone)]
39pub struct RefreshRequest {
40 pub refresh_token: Secret,
42 pub client_id: String,
44 pub scopes: Vec<String>,
47}
48
49impl RefreshRequest {
50 pub fn new(refresh_token: Secret) -> Self {
52 Self {
53 refresh_token,
54 client_id: CLAUDE_CODE_CLIENT_ID.to_owned(),
55 scopes: DEFAULT_SCOPES.iter().map(|s| (*s).to_owned()).collect(),
56 }
57 }
58
59 #[must_use]
61 pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
62 self.client_id = client_id.into();
63 self
64 }
65
66 #[must_use]
68 pub fn scopes<I, S>(mut self, scopes: I) -> Self
69 where
70 I: IntoIterator<Item = S>,
71 S: Into<String>,
72 {
73 let scopes: Vec<String> = scopes.into_iter().map(Into::into).collect();
74 if !scopes.is_empty() {
75 self.scopes = scopes;
76 }
77 self
78 }
79
80 pub fn scope_string(&self) -> String {
82 self.scopes.join(" ")
83 }
84}
85
86#[derive(Serialize)]
88pub(crate) struct GrantBody<'a> {
89 pub grant_type: &'static str,
90 pub refresh_token: &'a str,
91 pub client_id: &'a str,
92 pub scope: String,
93}
94
95#[derive(Deserialize)]
104pub(crate) struct GrantResponse {
105 pub access_token: Secret,
106 #[serde(default)]
107 pub refresh_token: Option<Secret>,
108 #[serde(default, deserialize_with = "lenient::seconds")]
109 pub expires_in: Option<u64>,
110 #[serde(default, deserialize_with = "lenient::seconds")]
111 pub refresh_token_expires_in: Option<u64>,
112 #[serde(default, deserialize_with = "lenient::opt")]
113 pub scope: Option<String>,
114 #[serde(default, deserialize_with = "lenient::opt")]
115 pub token_type: Option<String>,
116}
117
118#[derive(Debug, Clone)]
125#[non_exhaustive]
126pub struct TokenSet {
127 pub access_token: Secret,
129 pub refresh_token: Secret,
132 pub rotated: bool,
134 pub expires_in: Option<Duration>,
138 pub expires_at: Option<SystemTime>,
142 pub refresh_token_expires_at: Option<SystemTime>,
144 pub refresh_token_expires_in: Option<Duration>,
147 pub scopes: Vec<String>,
149 pub token_type: Option<String>,
151}
152
153impl TokenSet {
154 pub(crate) fn from_response(r: GrantResponse, posted: &Secret, now: SystemTime) -> Self {
155 let expires_in = r.expires_in.map(Duration::from_secs);
156 let refresh_token_expires_in = r.refresh_token_expires_in.map(Duration::from_secs);
157 let (refresh_token, rotated) = match r.refresh_token {
158 Some(t) if !t.is_empty() => (t, true),
159 _ => (posted.clone(), false),
160 };
161 Self {
162 access_token: r.access_token,
163 refresh_token,
164 rotated,
165 expires_in,
166 expires_at: expires_in.and_then(|d| now.checked_add(d)),
168 refresh_token_expires_at: refresh_token_expires_in.and_then(|d| now.checked_add(d)),
169 refresh_token_expires_in,
170 scopes: r
171 .scope
172 .unwrap_or_default()
173 .split_whitespace()
174 .map(ToOwned::to_owned)
175 .collect(),
176 token_type: r.token_type,
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 fn decode(body: &str) -> TokenSet {
186 let r: GrantResponse = serde_json::from_str(body).unwrap();
187 TokenSet::from_response(r, &Secret::new("posted"), SystemTime::UNIX_EPOCH)
188 }
189
190 #[test]
191 fn huge_expiry_does_not_panic() {
192 let t = decode(
193 r#"{"access_token":"a","refresh_token":"r","expires_in":18446744073709551615,"refresh_token_expires_in":18446744073709551615}"#,
194 );
195 assert_eq!(t.expires_in, Some(Duration::from_secs(u64::MAX)));
196 assert_eq!(t.expires_at, None);
197 assert_eq!(t.refresh_token_expires_at, None);
198 assert_eq!(
199 t.refresh_token_expires_in,
200 Some(Duration::from_secs(u64::MAX))
201 );
202 assert_eq!(t.refresh_token.expose(), "r");
203 }
204
205 #[test]
206 fn odd_fields_keep_the_tokens() {
207 for body in [
208 r#"{"access_token":"a","refresh_token":"r"}"#,
209 r#"{"access_token":"a","refresh_token":"r","expires_in":"3600"}"#,
210 r#"{"access_token":"a","refresh_token":"r","expires_in":3600.0}"#,
211 r#"{"access_token":"a","refresh_token":"r","expires_in":{"s":1},"scope":7,"token_type":[]}"#,
212 r#"{"access_token":"a","refresh_token":"r","expires_in":-5,"refresh_token_expires_in":"soon"}"#,
213 r#"{"access_token":"a","refresh_token":"r","expires_in":null,"new_field":{"x":[1,2]}}"#,
214 ] {
215 let t = decode(body);
216 assert_eq!(t.access_token.expose(), "a", "{body}");
217 assert_eq!(t.refresh_token.expose(), "r", "{body}");
218 assert!(t.rotated, "{body}");
219 }
220 let t = decode(r#"{"access_token":"a","refresh_token":"r","expires_in":"3600"}"#);
221 assert_eq!(t.expires_in, Some(Duration::from_secs(3600)));
222 assert_eq!(
223 t.expires_at,
224 Some(SystemTime::UNIX_EPOCH + Duration::from_secs(3600))
225 );
226 let t = decode(r#"{"access_token":"a","refresh_token":"r"}"#);
227 assert_eq!((t.expires_in, t.expires_at), (None, None));
228 assert_eq!(
229 (t.refresh_token_expires_in, t.refresh_token_expires_at),
230 (None, None)
231 );
232 }
233}