use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec::Vec;
use core::time::Duration;
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use crate::{Secret, lenient};
pub const CLAUDE_CODE_CLIENT_ID: &str = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
pub const DEFAULT_SCOPES: &[&str] = &[
"user:profile",
"user:inference",
"user:sessions:claude_code",
"user:mcp_servers",
"user:file_upload",
"user:plugins",
];
#[derive(Debug, Clone)]
pub struct RefreshRequest {
pub refresh_token: Secret,
pub client_id: String,
pub scopes: Vec<String>,
}
impl RefreshRequest {
pub fn new(refresh_token: Secret) -> Self {
Self {
refresh_token,
client_id: CLAUDE_CODE_CLIENT_ID.to_owned(),
scopes: DEFAULT_SCOPES.iter().map(|s| (*s).to_owned()).collect(),
}
}
#[must_use]
pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = client_id.into();
self
}
#[must_use]
pub fn scopes<I, S>(mut self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let scopes: Vec<String> = scopes.into_iter().map(Into::into).collect();
if !scopes.is_empty() {
self.scopes = scopes;
}
self
}
pub fn scope_string(&self) -> String {
self.scopes.join(" ")
}
}
#[derive(Serialize)]
pub(crate) struct GrantBody<'a> {
pub grant_type: &'static str,
pub refresh_token: &'a str,
pub client_id: &'a str,
pub scope: String,
}
#[derive(Deserialize)]
pub(crate) struct GrantResponse {
pub access_token: Secret,
#[serde(default)]
pub refresh_token: Option<Secret>,
#[serde(default, deserialize_with = "lenient::seconds")]
pub expires_in: Option<u64>,
#[serde(default, deserialize_with = "lenient::seconds")]
pub refresh_token_expires_in: Option<u64>,
#[serde(default, deserialize_with = "lenient::opt")]
pub scope: Option<String>,
#[serde(default, deserialize_with = "lenient::opt")]
pub token_type: Option<String>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct TokenSet {
pub access_token: Secret,
pub refresh_token: Secret,
pub rotated: bool,
pub expires_in: Option<Duration>,
pub expires_at: Option<SystemTime>,
pub refresh_token_expires_at: Option<SystemTime>,
pub refresh_token_expires_in: Option<Duration>,
pub scopes: Vec<String>,
pub token_type: Option<String>,
}
impl TokenSet {
pub(crate) fn from_response(r: GrantResponse, posted: &Secret, now: SystemTime) -> Self {
let expires_in = r.expires_in.map(Duration::from_secs);
let refresh_token_expires_in = r.refresh_token_expires_in.map(Duration::from_secs);
let (refresh_token, rotated) = match r.refresh_token {
Some(t) if !t.is_empty() => (t, true),
_ => (posted.clone(), false),
};
Self {
access_token: r.access_token,
refresh_token,
rotated,
expires_in,
expires_at: expires_in.and_then(|d| now.checked_add(d)),
refresh_token_expires_at: refresh_token_expires_in.and_then(|d| now.checked_add(d)),
refresh_token_expires_in,
scopes: r
.scope
.unwrap_or_default()
.split_whitespace()
.map(ToOwned::to_owned)
.collect(),
token_type: r.token_type,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn decode(body: &str) -> TokenSet {
let r: GrantResponse = serde_json::from_str(body).unwrap();
TokenSet::from_response(r, &Secret::new("posted"), SystemTime::UNIX_EPOCH)
}
#[test]
fn huge_expiry_does_not_panic() {
let t = decode(
r#"{"access_token":"a","refresh_token":"r","expires_in":18446744073709551615,"refresh_token_expires_in":18446744073709551615}"#,
);
assert_eq!(t.expires_in, Some(Duration::from_secs(u64::MAX)));
assert_eq!(t.expires_at, None);
assert_eq!(t.refresh_token_expires_at, None);
assert_eq!(
t.refresh_token_expires_in,
Some(Duration::from_secs(u64::MAX))
);
assert_eq!(t.refresh_token.expose(), "r");
}
#[test]
fn odd_fields_keep_the_tokens() {
for body in [
r#"{"access_token":"a","refresh_token":"r"}"#,
r#"{"access_token":"a","refresh_token":"r","expires_in":"3600"}"#,
r#"{"access_token":"a","refresh_token":"r","expires_in":3600.0}"#,
r#"{"access_token":"a","refresh_token":"r","expires_in":{"s":1},"scope":7,"token_type":[]}"#,
r#"{"access_token":"a","refresh_token":"r","expires_in":-5,"refresh_token_expires_in":"soon"}"#,
r#"{"access_token":"a","refresh_token":"r","expires_in":null,"new_field":{"x":[1,2]}}"#,
] {
let t = decode(body);
assert_eq!(t.access_token.expose(), "a", "{body}");
assert_eq!(t.refresh_token.expose(), "r", "{body}");
assert!(t.rotated, "{body}");
}
let t = decode(r#"{"access_token":"a","refresh_token":"r","expires_in":"3600"}"#);
assert_eq!(t.expires_in, Some(Duration::from_secs(3600)));
assert_eq!(
t.expires_at,
Some(SystemTime::UNIX_EPOCH + Duration::from_secs(3600))
);
let t = decode(r#"{"access_token":"a","refresh_token":"r"}"#);
assert_eq!((t.expires_in, t.expires_at), (None, None));
assert_eq!(
(t.refresh_token_expires_in, t.refresh_token_expires_at),
(None, None)
);
}
}