const SERVICE: &str = "scitadel";
#[derive(Debug)]
pub struct MissingCredential {
pub source: String,
pub keys: Vec<String>,
pub env_vars: Vec<String>,
pub remedy: String,
}
impl std::fmt::Display for MissingCredential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{source} credentials not configured.\n\n\
To authenticate, run:\n scitadel auth login {source}\n\n\
Or set environment variable(s):\n{env_hint}",
source = self.source,
env_hint = self
.env_vars
.iter()
.map(|v| format!(" {v}=<value>"))
.collect::<Vec<_>>()
.join("\n"),
)
}
}
pub fn resolve(keychain_key: &str, env_var: &str, config_fallback: &str) -> Option<String> {
if let Some(val) = get_keychain(keychain_key) {
return Some(val);
}
if let Ok(val) = std::env::var(env_var)
&& !val.is_empty()
{
return Some(val);
}
if !config_fallback.is_empty() {
return Some(config_fallback.to_string());
}
None
}
pub fn store(key: &str, value: &str) -> Result<(), String> {
let _ = std::process::Command::new("security")
.args(["delete-generic-password", "-s", SERVICE, "-a", key])
.output();
let output = std::process::Command::new("security")
.args([
"add-generic-password",
"-s",
SERVICE,
"-a",
key,
"-w",
value,
"-U", ])
.output()
.map_err(|e| format!("failed to run security CLI: {e}"))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!("failed to store credential '{key}': {stderr}"))
}
}
pub fn delete(key: &str) -> Result<(), String> {
let output = std::process::Command::new("security")
.args(["delete-generic-password", "-s", SERVICE, "-a", key])
.output()
.map_err(|e| format!("failed to run security CLI: {e}"))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!("failed to delete credential '{key}': {stderr}"))
}
}
pub fn get_keychain(key: &str) -> Option<String> {
let output = std::process::Command::new("security")
.args(["find-generic-password", "-s", SERVICE, "-a", key, "-w"])
.output()
.ok()?;
if output.status.success() {
let val = String::from_utf8_lossy(&output.stdout).trim().to_string();
if val.is_empty() { None } else { Some(val) }
} else {
None
}
}
pub struct SourceCredentials {
pub source: &'static str,
pub keys: &'static [CredentialKey],
}
pub struct CredentialKey {
pub keychain_key: &'static str,
pub env_var: &'static str,
pub label: &'static str,
pub secret: bool,
}
pub static PATENTSVIEW_CREDENTIALS: SourceCredentials = SourceCredentials {
source: "patentsview",
keys: &[CredentialKey {
keychain_key: "patentsview.api_key",
env_var: "SCITADEL_PATENTSVIEW_KEY",
label: "API key",
secret: true,
}],
};
pub static PUBMED_CREDENTIALS: SourceCredentials = SourceCredentials {
source: "pubmed",
keys: &[CredentialKey {
keychain_key: "pubmed.api_key",
env_var: "SCITADEL_PUBMED_API_KEY",
label: "API key",
secret: true,
}],
};
pub static OPENALEX_CREDENTIALS: SourceCredentials = SourceCredentials {
source: "openalex",
keys: &[CredentialKey {
keychain_key: "openalex.email",
env_var: "SCITADEL_OPENALEX_EMAIL",
label: "Email (for polite pool)",
secret: false,
}],
};
pub static LENS_CREDENTIALS: SourceCredentials = SourceCredentials {
source: "lens",
keys: &[CredentialKey {
keychain_key: "lens.api_token",
env_var: "SCITADEL_LENS_TOKEN",
label: "API token",
secret: true,
}],
};
pub static EPO_CREDENTIALS: SourceCredentials = SourceCredentials {
source: "epo",
keys: &[
CredentialKey {
keychain_key: "epo.consumer_key",
env_var: "SCITADEL_EPO_KEY",
label: "Consumer key",
secret: false,
},
CredentialKey {
keychain_key: "epo.consumer_secret",
env_var: "SCITADEL_EPO_SECRET",
label: "Consumer secret",
secret: true,
},
],
};
pub static ALL_SOURCES: &[&SourceCredentials] = &[
&PUBMED_CREDENTIALS,
&OPENALEX_CREDENTIALS,
&PATENTSVIEW_CREDENTIALS,
&LENS_CREDENTIALS,
&EPO_CREDENTIALS,
];
pub fn check_source(creds: &SourceCredentials) -> Result<(), MissingCredential> {
let missing: Vec<&CredentialKey> = creds
.keys
.iter()
.filter(|k| resolve(k.keychain_key, k.env_var, "").is_none())
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(MissingCredential {
source: creds.source.to_string(),
keys: missing.iter().map(|k| k.keychain_key.to_string()).collect(),
env_vars: missing.iter().map(|k| k.env_var.to_string()).collect(),
remedy: format!("scitadel auth login {}", creds.source),
})
}
}