use super::{configfile, document, live, paths as claude};
use crate::api::{self, ApiError};
use crate::context::Context;
use crate::provider::{
Adoption, Credential, Expiry, Identity, Isolation, LiveStore, ParkSemantics, Provider,
ProviderError, ProviderId,
};
use crate::switch;
use crate::usage;
use serde_json::Value;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Claude;
const ADOPTION_SECONDS: u32 = switch::ADOPTION_CEILING_SECONDS;
impl Provider for Claude {
fn id(&self) -> ProviderId {
ProviderId::Claude
}
fn live(&self, ctx: &Context) -> Result<LiveStore, ProviderError> {
Ok(LiveStore {
chain: live::chain(ctx),
service: claude::live_service(ctx),
})
}
fn identify(&self, ctx: &Context, credential: &Credential) -> Result<Identity, ProviderError> {
let token =
access_token(&credential.raw).ok_or_else(|| ProviderError::ShapeUnexpected {
provider: ProviderId::Claude,
detail: "it has no claudeAiOauth.accessToken".into(),
})?;
api::owner(ctx, token).map(from_owner).map_err(from_api)
}
fn usage(
&self,
ctx: &Context,
credential: &Credential,
) -> Result<usage::Snapshot, ProviderError> {
let token =
access_token(&credential.raw).ok_or_else(|| ProviderError::ShapeUnexpected {
provider: ProviderId::Claude,
detail: "it has no claudeAiOauth.accessToken".into(),
})?;
api::usage(ctx, token).map_err(from_api)
}
fn renew(&self, ctx: &Context, credential: &Credential) -> Result<Credential, ProviderError> {
let oauth = document::oauth_in(&credential.raw);
let refresh = oauth["refreshToken"].as_str().unwrap_or_default();
let mut scopes: Vec<String> = oauth["scopes"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect();
if scopes.is_empty() {
scopes = switch::renew::DEFAULT_SCOPES.map(str::to_owned).to_vec();
}
let client_id = oauth["clientId"].as_str();
let fresh = api::renew(ctx, refresh, &scopes, client_id).map_err(from_api)?;
let at_millis = fresh.at.map_or_else(|| ctx.now_millis(), |at| at * 1000);
Ok(Credential::new(
ProviderId::Claude,
document::renewed(&credential.raw, &fresh, at_millis),
))
}
fn slot(&self, ctx: &Context) -> String {
claude::live_service(ctx)
}
fn write_lock(&self, ctx: &Context) -> Option<PathBuf> {
Some(PathBuf::from(claude::storage_dir(ctx)).join(".storage-write"))
}
fn recorded_identity(&self, ctx: &Context) -> Option<Identity> {
let config = claude::load_config(ctx).ok()?;
let found = claude::identity(&config)?;
Some(Identity {
account_id: found.account_uuid,
email: found.email,
group: Some(found.organization_uuid).filter(|o| !o.is_empty()),
})
}
fn after_switch(
&self,
ctx: &Context,
incoming: &crate::state::Account,
outgoing: &Identity,
) -> Result<(), crate::error::Error> {
let path = claude::config_file(ctx);
let outgoing_group = outgoing.group.clone().unwrap_or_default();
configfile::backup(ctx, &path)?;
configfile::update(ctx, &path, |config| {
configfile::splice_identity(
config,
incoming.claude().map_or(&Value::Null, |c| c.oauth_account),
&[outgoing.account_id.as_str(), outgoing_group.as_str()],
)
})
.map(|_| ())
}
fn program(&self, ctx: &Context) -> Option<PathBuf> {
claude::program(ctx)
}
fn sign_in(&self, ctx: &Context, dir: &std::path::Path) -> std::process::Command {
let mut command = crate::provider::command(ctx, ProviderId::Claude);
command
.args(["auth", "login"])
.env("CLAUDE_CONFIG_DIR", dir)
.env_remove("CLAUDE_SECURESTORAGE_CONFIG_DIR");
command
}
fn read_signin(
&self,
ctx: &Context,
dir: &std::path::Path,
) -> Result<Option<String>, crate::store::Error> {
live::read_signin(ctx, dir)
}
fn discard_signin(&self, ctx: &Context, dir: &std::path::Path) {
let _ = live::discard_signin(ctx, dir);
}
fn overridden_by(&self, ctx: &Context) -> Vec<String> {
crate::settings::overrides(ctx)
.iter()
.map(ToString::to_string)
.collect()
}
fn adoption(&self) -> Adoption {
Adoption::PollingWithin(ADOPTION_SECONDS)
}
fn park_semantics(&self) -> ParkSemantics {
ParkSemantics::CopyWhileLive
}
fn private_signin_isolation(&self, _ctx: &Context) -> Isolation {
Isolation::Isolated
}
fn slice(&self, live: &Value) -> Result<Value, ProviderError> {
if live.is_object() && live.get("claudeAiOauth").is_none() {
return Err(ProviderError::NoLogin {
provider: ProviderId::Claude,
});
}
document::slice(live).map_err(|detail| ProviderError::ShapeUnexpected {
provider: ProviderId::Claude,
detail,
})
}
fn splice(&self, live: &Value, incoming: &Value) -> Result<Value, ProviderError> {
document::splice(live, incoming).map_err(|detail| ProviderError::ShapeUnexpected {
provider: ProviderId::Claude,
detail,
})
}
fn fingerprint(&self, slice: &Value) -> String {
document::fingerprint_of(slice)
}
fn expiry(&self, slice: &Value) -> Expiry {
let oauth = document::oauth_in(slice);
let at = |key: &str| oauth.get(key).and_then(Value::as_i64).map(|ms| ms / 1000);
Expiry {
access_expires_at: at("expiresAt"),
refresh_expires_at: at("refreshTokenExpiresAt"),
}
}
}
fn access_token(login: &Value) -> Option<&str> {
document::oauth_in(login)
.get("accessToken")
.and_then(Value::as_str)
.filter(|token| !token.is_empty())
}
fn from_owner(owner: api::Owner) -> Identity {
Identity {
account_id: owner.account_uuid,
email: owner.email,
group: Some(owner.organization_uuid).filter(|o| !o.is_empty()),
}
}
fn from_api(error: ApiError) -> ProviderError {
match error {
ApiError::Unauthorized => ProviderError::Unauthorized,
ApiError::RateLimited { retry_after } => ProviderError::RateLimited {
service: ProviderId::Claude.service(),
retry_after,
},
ApiError::Network(detail) => ProviderError::Network {
service: ProviderId::Claude.service(),
detail,
},
ApiError::Unexpected { status } => ProviderError::Unexpected {
service: ProviderId::Claude.service(),
status,
},
ApiError::Malformed(detail) => ProviderError::Malformed {
service: ProviderId::Claude.service(),
detail,
},
ApiError::InvalidGrant => ProviderError::InvalidGrant {
service: ProviderId::Claude.service(),
},
}
}