use std::path::PathBuf;
const ENV_API_KEY: &str = "ANTHROPIC_API_KEY";
const ENV_AUTH_TOKEN: &str = "ANTHROPIC_AUTH_TOKEN";
#[cfg(test)]
use crate::hooks::claude_code::CONFIG_DIR_ENV;
const SEAM_STATE_FILE: &str = "OPENLATCH_TEST_CLAUDE_STATE_FILE";
const SEAM_SETTINGS_FILE: &str = "OPENLATCH_TEST_CLAUDE_SETTINGS_FILE";
const SHARED_KEY: &str = "shared-key";
const MAX_STATE_BYTES: usize = 8 * 1024 * 1024;
const MAX_SETTINGS_BYTES: usize = 1024 * 1024;
pub async fn resolve() -> Option<String> {
if shared_key_in_env() || shared_key_in_settings().await {
return Some(SHARED_KEY.to_string());
}
account_email().await
}
fn shared_key_in_env() -> bool {
[ENV_API_KEY, ENV_AUTH_TOKEN]
.iter()
.any(|key| std::env::var_os(key).is_some_and(|v| !v.is_empty()))
}
async fn shared_key_in_settings() -> bool {
let Some(path) = settings_path() else {
return false;
};
let Some(value) = read_json(&path, MAX_SETTINGS_BYTES).await else {
return false;
};
let Some(obj) = value.as_object() else {
return false;
};
if obj.contains_key("apiKeyHelper") {
return true;
}
obj.get("env")
.and_then(|e| e.as_object())
.is_some_and(|env| {
[ENV_API_KEY, ENV_AUTH_TOKEN].iter().any(|key| {
env.get(*key)
.and_then(|v| v.as_str())
.is_some_and(|v| !v.is_empty())
})
})
}
async fn account_email() -> Option<String> {
let path = state_path()?;
let value = read_json(&path, MAX_STATE_BYTES).await?;
let email = value
.get("oauthAccount")
.and_then(|account| account.get("emailAddress"))
.and_then(serde_json::Value::as_str)
.filter(|email| !email.is_empty());
if email.is_none() {
debug_absent("no_account");
}
email.map(str::to_owned)
}
fn state_path() -> Option<PathBuf> {
if let Some(seam) = env_path(SEAM_STATE_FILE) {
return Some(seam);
}
Some(crate::hooks::claude_code::state_dir()?.join(".claude.json"))
}
fn settings_path() -> Option<PathBuf> {
if let Some(seam) = env_path(SEAM_SETTINGS_FILE) {
return Some(seam);
}
Some(crate::hooks::claude_code::config_dir()?.join("settings.json"))
}
fn env_path(key: &str) -> Option<PathBuf> {
std::env::var_os(key)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
async fn read_json(path: &std::path::Path, max_bytes: usize) -> Option<serde_json::Value> {
let Ok(meta) = tokio::fs::metadata(path).await else {
debug_absent("absent");
return None;
};
if meta.len() > max_bytes as u64 {
debug_absent("unreadable");
return None;
}
let Ok(bytes) = tokio::fs::read(path).await else {
debug_absent("unreadable");
return None;
};
match serde_json::from_slice(&bytes) {
Ok(value) => Some(value),
Err(_) => {
debug_absent("unreadable");
None
}
}
}
fn debug_absent(reason: &'static str) {
tracing::debug!(target: "identity", reason, "provideracct not resolved");
}
#[cfg(test)]
mod tests {
use super::*;
use crate::daemon::identity::test_support::EnvGuard;
use std::path::Path;
fn write(dir: &Path, name: &str, body: &str) -> PathBuf {
let path = dir.join(name);
std::fs::write(&path, body).expect("write fixture");
path
}
#[tokio::test]
async fn provider_account_matrix() {
let _lock = crate::daemon::identity::ENV_LOCK.lock().await;
let env = EnvGuard::clear();
let dir = tempfile::tempdir().expect("tempdir");
let state = write(
dir.path(),
"state.json",
r#"{"oauthAccount":{"emailAddress":"alice@fixture.test"}}"#,
);
env.set(SEAM_STATE_FILE, &state);
assert_eq!(resolve().await.as_deref(), Some("alice@fixture.test"));
for key in [ENV_API_KEY, ENV_AUTH_TOKEN] {
env.set(key, "sk-ant-fixture-value-never-read");
assert_eq!(
resolve().await.as_deref(),
Some(SHARED_KEY),
"{key} must win over a cached oauthAccount"
);
env.unset(key);
}
env.set(ENV_API_KEY, "");
assert_eq!(resolve().await.as_deref(), Some("alice@fixture.test"));
env.unset(ENV_API_KEY);
let helper = write(
dir.path(),
"settings-helper.json",
r#"{"apiKeyHelper":"x"}"#,
);
env.set(SEAM_SETTINGS_FILE, &helper);
assert_eq!(resolve().await.as_deref(), Some(SHARED_KEY));
let env_block = write(
dir.path(),
"settings-env-block.json",
r#"{"env":{"ANTHROPIC_API_KEY":"sk-ant-fixture-value-never-read"}}"#,
);
env.set(SEAM_SETTINGS_FILE, &env_block);
assert_eq!(
resolve().await.as_deref(),
Some(SHARED_KEY),
"settings env.ANTHROPIC_API_KEY must win over a cached oauthAccount"
);
let env_block_empty = write(
dir.path(),
"settings-env-empty.json",
r#"{"env":{"ANTHROPIC_API_KEY":""}}"#,
);
env.set(SEAM_SETTINGS_FILE, &env_block_empty);
assert_eq!(
resolve().await.as_deref(),
Some("alice@fixture.test"),
"an empty env-block value is not a credential"
);
let plain = write(dir.path(), "settings-plain.json", "{}");
env.set(SEAM_SETTINGS_FILE, &plain);
assert_eq!(resolve().await.as_deref(), Some("alice@fixture.test"));
for (name, body) in [
("malformed.json", "{not json"),
("no-account.json", r#"{"someOtherKey":true}"#),
(
"empty-email.json",
r#"{"oauthAccount":{"emailAddress":""}}"#,
),
("wrong-type.json", r#"{"oauthAccount":{"emailAddress":42}}"#),
] {
let path = write(dir.path(), name, body);
env.set(SEAM_STATE_FILE, &path);
assert_eq!(resolve().await, None, "{name} must resolve to absent");
}
env.set(SEAM_STATE_FILE, dir.path().join("does-not-exist.json"));
assert_eq!(
resolve().await,
None,
"an absent file is an absent attribute"
);
let collision = write(
dir.path(),
"case.json",
r#"{"oauthaccount":{"emailAddress":"wrong@fixture.test"},
"oauthAccount":{"emailAddress":"alice@fixture.test"}}"#,
);
env.set(SEAM_STATE_FILE, &collision);
assert_eq!(resolve().await.as_deref(), Some("alice@fixture.test"));
}
#[tokio::test]
async fn a_relocated_config_dir_never_falls_back_to_home() {
let _lock = crate::daemon::identity::ENV_LOCK.lock().await;
let env = EnvGuard::clear();
let dir = tempfile::tempdir().expect("tempdir");
env.set(CONFIG_DIR_ENV, dir.path());
assert_eq!(
resolve().await,
None,
"an empty relocated config dir must not inherit another profile's human"
);
write(
dir.path(),
".claude.json",
r#"{"oauthAccount":{"emailAddress":"relocated@fixture.test"}}"#,
);
assert_eq!(resolve().await.as_deref(), Some("relocated@fixture.test"));
env.set(CONFIG_DIR_ENV, "");
assert_ne!(
resolve().await.as_deref(),
Some("relocated@fixture.test"),
"an empty value must not keep pointing at the relocated dir"
);
}
}