use crate::api;
use crate::store;
use serde_json::{Value, json};
pub(crate) const ACCOUNT_SCOPED: [&str; 4] = [
"organizationUuid",
"trustedDeviceToken",
"enterpriseGateway",
"designOauth",
];
pub(crate) fn slice(document: &Value) -> Result<Value, String> {
let object = document
.as_object()
.ok_or_else(|| "it is not a JSON object".to_string())?;
let oauth = object
.get("claudeAiOauth")
.ok_or_else(|| "it has no claudeAiOauth block".to_string())?;
let mut slice = serde_json::Map::new();
slice.insert("claudeAiOauth".into(), oauth.clone());
for key in ACCOUNT_SCOPED {
if let Some(value) = object.get(key) {
slice.insert(key.into(), value.clone());
}
}
Ok(Value::Object(slice))
}
pub(crate) fn splice(before: &Value, incoming: &Value) -> Result<Value, String> {
let mut next = before.clone();
let document = next
.as_object_mut()
.ok_or_else(|| "it is not a JSON object".to_string())?;
document.insert("claudeAiOauth".into(), oauth_in(incoming).clone());
for key in ACCOUNT_SCOPED {
match incoming.get(key) {
Some(value) => document.insert(key.into(), value.clone()),
None => document.remove(key),
};
}
Ok(next)
}
pub(crate) fn oauth_in(document: &Value) -> &Value {
document.get("claudeAiOauth").unwrap_or(document)
}
pub(crate) fn fingerprint_of(document: &Value) -> String {
oauth_in(document)
.get("refreshToken")
.and_then(Value::as_str)
.map(store::fingerprint)
.unwrap_or_default()
}
pub(crate) fn renewed(document: &Value, fresh: &api::Renewed, now_millis: i64) -> Value {
let mut next = document.clone();
let oauth = match next.get_mut("claudeAiOauth") {
Some(block) => block,
None => &mut next,
};
let Some(fields) = oauth.as_object_mut() else {
return next;
};
fields.insert("accessToken".into(), json!(fresh.access_token));
if let Some(refresh) = &fresh.refresh_token {
fields.insert("refreshToken".into(), json!(refresh));
}
fields.insert(
"expiresAt".into(),
json!(now_millis + fresh.expires_in * 1000),
);
if let Some(seconds) = fresh.refresh_token_expires_in {
fields.insert(
"refreshTokenExpiresAt".into(),
json!(now_millis + seconds * 1000),
);
}
if let Some(scopes) = &fresh.scopes {
fields.insert("scopes".into(), json!(scopes));
}
next
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn what_is_parked_is_everything_that_belongs_to_the_account() {
let live = json!({
"claudeAiOauth": {"refreshToken": "a"},
"organizationUuid": "org-a",
"trustedDeviceToken": "device-of-a",
"enterpriseGateway": {"url": "https://gateway.example"},
"designOauth": {"refreshToken": "design-of-a"},
"mcpOAuth": {"a-server": "token"},
"somethingOfThisMachine": true,
});
assert_eq!(
slice(&live).expect("it has an oauth block"),
json!({
"claudeAiOauth": {"refreshToken": "a"},
"organizationUuid": "org-a",
"trustedDeviceToken": "device-of-a",
"enterpriseGateway": {"url": "https://gateway.example"},
"designOauth": {"refreshToken": "design-of-a"},
}),
"everything the account owns, and nothing the machine or another server owns"
);
}
#[test]
fn restoring_a_slice_replaces_the_outgoing_accounts_keys_rather_than_only_removing_them() {
let before = json!({
"claudeAiOauth": {"refreshToken": "a"},
"organizationUuid": "org-a",
"trustedDeviceToken": "device-of-a",
"designOauth": {"refreshToken": "design-of-a"},
"mcpOAuth": {"a-server": "token"},
});
let incoming = json!({
"claudeAiOauth": {"refreshToken": "b"},
"organizationUuid": "org-b",
"trustedDeviceToken": "device-of-b",
});
let after = splice(&before, &incoming).expect("spliced");
assert_eq!(after["claudeAiOauth"]["refreshToken"], "b");
assert_eq!(after["organizationUuid"], "org-b");
assert_eq!(after["trustedDeviceToken"], "device-of-b");
assert!(
after.get("designOauth").is_none(),
"a key the incoming account does not have must not be left holding the \
outgoing account's value"
);
assert_eq!(
after["mcpOAuth"]["a-server"], "token",
"and what belongs to neither account stays"
);
}
#[test]
fn a_park_from_before_the_slice_still_restores() {
let before = json!({
"claudeAiOauth": {"refreshToken": "a"},
"organizationUuid": "org-a",
"trustedDeviceToken": "device-of-a",
});
let legacy = json!({"refreshToken": "b", "accessToken": "b-access"});
let after = splice(&before, &legacy).expect("spliced");
assert_eq!(after["claudeAiOauth"]["refreshToken"], "b");
assert!(after.get("organizationUuid").is_none());
assert!(after.get("trustedDeviceToken").is_none());
}
#[test]
fn a_switch_leaves_nothing_of_the_outgoing_account() {
let before = json!({
"claudeAiOauth": {"refreshToken": "old"},
"organizationUuid": "org-a",
"trustedDeviceToken": "device-of-a",
"enterpriseGateway": {"url": "https://gateway.example"},
"designOauth": {"refreshToken": "design-of-a"},
"somethingOfThisMachine": true,
});
let after = splice(&before, &json!({"refreshToken": "new"})).expect("spliced");
assert_eq!(after["claudeAiOauth"]["refreshToken"], "new");
assert_eq!(after["somethingOfThisMachine"], true);
for key in ACCOUNT_SCOPED {
assert!(after.get(key).is_none(), "{key} was left behind");
}
}
#[test]
fn a_credential_without_claude_ai_oauth_is_refused() {
assert!(slice(&json!({"slackTag": {}})).is_err());
assert!(slice(&json!({"claudeAiOauth": {"accessToken": "a"}})).is_ok());
}
#[test]
fn a_renewed_login_is_stored_as_claude_code_stores_its_own() {
let parked = json!({
"accessToken": "a1", "refreshToken": "r1", "expiresAt": 1,
"refreshTokenExpiresAt": 2, "scopes": ["user:inference"],
"subscriptionType": "max", "rateLimitTier": "default_claude_max_20x"
});
let fresh = api::Renewed {
access_token: "a2".into(),
refresh_token: Some("r2".into()),
expires_in: 60,
refresh_token_expires_in: Some(120),
scopes: None,
at: None,
};
let next = renewed(&parked, &fresh, 1_000_000);
assert_eq!(next["accessToken"], "a2");
assert_eq!(next["refreshToken"], "r2");
assert_eq!(next["expiresAt"], 1_060_000);
assert_eq!(next["refreshTokenExpiresAt"], 1_120_000);
assert_eq!(
next["scopes"],
json!(["user:inference"]),
"kept when not answered"
);
assert_eq!(
next["subscriptionType"], "max",
"what renewal does not touch stays"
);
let kept = api::Renewed {
refresh_token: None,
refresh_token_expires_in: None,
..fresh
};
let next = renewed(&parked, &kept, 1_000_000);
assert_eq!(
next["refreshToken"], "r1",
"the server kept the refresh token"
);
assert_eq!(next["refreshTokenExpiresAt"], 2);
}
#[test]
fn a_credential_with_no_refresh_token_fingerprints_to_nothing_rather_than_panicking() {
assert_eq!(fingerprint_of(&json!({"accessToken": "a"})), "");
}
}