use super::{credman, paths as claude, slot};
use crate::context::Context;
use crate::host::{OS, Os};
use crate::service::Permit;
use crate::store::{self, Backend, Error, Live, RawStore, Refused};
use serde_json::Value;
use std::path::{Path, PathBuf};
pub(crate) fn credential_file(ctx: &Context) -> PathBuf {
PathBuf::from(claude::storage_dir(ctx)).join(slot::CRED_FILE)
}
pub(crate) fn chain(ctx: &Context) -> Live {
let host = ctx.host();
match OS {
Os::MacOs | Os::Linux => {
let mut backends: Vec<Box<dyn RawStore>> = Vec::new();
if let Some(keychain) = host.foreign_secrets(ctx, &slot::account_name(ctx)) {
backends.push(keychain);
}
backends.push(host.file(credential_file(ctx)));
Live::of(backends)
}
Os::Windows if credman::turned_on(ctx).is_some() => Live::taken_over_by_the_first(vec![
Box::new(credman::Store::of(ctx)),
host.file(credential_file(ctx)),
]),
Os::Windows => match credman::ambiguity(ctx) {
None => Live::of(vec![host.file(credential_file(ctx))]),
Some(refused) => Live::of(vec![Box::new(Refused::new(Backend::Unknown, refused))]),
},
}
}
pub(crate) fn behind(ctx: &Context) -> Option<Result<String, Error>> {
let (live, service) = (chain(ctx), claude::live_service(ctx));
let file = store::behind(&live, &service).ok()??;
file.read(&service).transpose()
}
pub(crate) fn document_in(held: &str) -> Value {
serde_json::from_str::<Value>(held)
.ok()
.filter(Value::is_object)
.unwrap_or_else(|| Value::Object(serde_json::Map::new()))
}
pub(crate) fn holds_a_login(document: &Value) -> bool {
let oauth = document.get("claudeAiOauth");
["accessToken", "refreshToken"].into_iter().any(|token| {
oauth
.and_then(|oauth| oauth.get(token))
.and_then(Value::as_str)
.is_some_and(|token| !token.is_empty())
})
}
pub(crate) fn login_in(held: &str) -> Option<Value> {
Some(document_in(held)).filter(holds_a_login)
}
pub(crate) fn read_signin(ctx: &Context, dir: &Path) -> Result<Option<String>, Error> {
let service = slot::service_for_dir(&dir.to_string_lossy());
let file = ctx.host().file(dir.join(slot::CRED_FILE));
match OS {
Os::MacOs | Os::Linux => match ctx.host().foreign_secrets(ctx, &slot::account_name(ctx)) {
Some(keychain) => keychain.read(&service),
None => file.read(""),
},
Os::Windows => {
let left = Live::of(vec![Box::new(credman::Store::of(ctx)), file]);
store::read_raw(&left, &service)
}
}
}
pub(crate) fn discard_signin(ctx: &Context, permit: Permit, dir: &Path) -> Result<(), Error> {
let service = slot::service_for_dir(&dir.to_string_lossy());
if service == slot::LIVE_SERVICE || service == claude::live_service(ctx) {
return Err(Error::Write(format!("refusing to delete {service}")));
}
let file = ctx.host().file(dir.join(slot::CRED_FILE));
match OS {
Os::MacOs | Os::Linux => match ctx.host().foreign_secrets(ctx, &slot::account_name(ctx)) {
Some(keychain) => keychain.delete(permit, &service),
None => file.delete(permit, ""),
},
Os::Windows => {
credman::Store::of(ctx).delete(permit, &service)?;
file.delete(permit, "")
}
}
}