1const SERVICE: &str = "scitadel";
9
10#[derive(Debug)]
12pub struct MissingCredential {
13 pub source: String,
14 pub keys: Vec<String>,
15 pub env_vars: Vec<String>,
16 pub remedy: String,
17}
18
19impl std::fmt::Display for MissingCredential {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 write!(
22 f,
23 "{source} credentials not configured.\n\n\
24 To authenticate, run:\n scitadel auth login {source}\n\n\
25 Or set environment variable(s):\n{env_hint}",
26 source = self.source,
27 env_hint = self
28 .env_vars
29 .iter()
30 .map(|v| format!(" {v}=<value>"))
31 .collect::<Vec<_>>()
32 .join("\n"),
33 )
34 }
35}
36
37pub fn resolve(keychain_key: &str, env_var: &str, config_fallback: &str) -> Option<String> {
41 if let Some(val) = get_keychain(keychain_key) {
43 return Some(val);
44 }
45
46 if let Ok(val) = std::env::var(env_var)
48 && !val.is_empty()
49 {
50 return Some(val);
51 }
52
53 if !config_fallback.is_empty() {
55 return Some(config_fallback.to_string());
56 }
57
58 None
59}
60
61pub fn store(key: &str, value: &str) -> Result<(), String> {
63 let _ = std::process::Command::new("security")
65 .args(["delete-generic-password", "-s", SERVICE, "-a", key])
66 .output();
67
68 let output = std::process::Command::new("security")
69 .args([
70 "add-generic-password",
71 "-s",
72 SERVICE,
73 "-a",
74 key,
75 "-w",
76 value,
77 "-U", ])
79 .output()
80 .map_err(|e| format!("failed to run security CLI: {e}"))?;
81
82 if output.status.success() {
83 Ok(())
84 } else {
85 let stderr = String::from_utf8_lossy(&output.stderr);
86 Err(format!("failed to store credential '{key}': {stderr}"))
87 }
88}
89
90pub fn delete(key: &str) -> Result<(), String> {
92 let output = std::process::Command::new("security")
93 .args(["delete-generic-password", "-s", SERVICE, "-a", key])
94 .output()
95 .map_err(|e| format!("failed to run security CLI: {e}"))?;
96
97 if output.status.success() {
98 Ok(())
99 } else {
100 let stderr = String::from_utf8_lossy(&output.stderr);
101 Err(format!("failed to delete credential '{key}': {stderr}"))
102 }
103}
104
105pub fn get_keychain(key: &str) -> Option<String> {
110 let output = std::process::Command::new("security")
111 .args(["find-generic-password", "-s", SERVICE, "-a", key, "-w"])
112 .output()
113 .ok()?;
114
115 if output.status.success() {
116 let val = String::from_utf8_lossy(&output.stdout).trim().to_string();
117 if val.is_empty() { None } else { Some(val) }
118 } else {
119 None
120 }
121}
122
123pub struct SourceCredentials {
125 pub source: &'static str,
126 pub keys: &'static [CredentialKey],
127}
128
129pub struct CredentialKey {
130 pub keychain_key: &'static str,
131 pub env_var: &'static str,
132 pub label: &'static str,
133 pub secret: bool,
134}
135
136pub static PATENTSVIEW_CREDENTIALS: SourceCredentials = SourceCredentials {
137 source: "patentsview",
138 keys: &[CredentialKey {
139 keychain_key: "patentsview.api_key",
140 env_var: "SCITADEL_PATENTSVIEW_KEY",
141 label: "API key",
142 secret: true,
143 }],
144};
145
146pub static PUBMED_CREDENTIALS: SourceCredentials = SourceCredentials {
147 source: "pubmed",
148 keys: &[CredentialKey {
149 keychain_key: "pubmed.api_key",
150 env_var: "SCITADEL_PUBMED_API_KEY",
151 label: "API key",
152 secret: true,
153 }],
154};
155
156pub static OPENALEX_CREDENTIALS: SourceCredentials = SourceCredentials {
157 source: "openalex",
158 keys: &[CredentialKey {
159 keychain_key: "openalex.email",
160 env_var: "SCITADEL_OPENALEX_EMAIL",
161 label: "Email (for polite pool)",
162 secret: false,
163 }],
164};
165
166pub static LENS_CREDENTIALS: SourceCredentials = SourceCredentials {
167 source: "lens",
168 keys: &[CredentialKey {
169 keychain_key: "lens.api_token",
170 env_var: "SCITADEL_LENS_TOKEN",
171 label: "API token",
172 secret: true,
173 }],
174};
175
176pub static EPO_CREDENTIALS: SourceCredentials = SourceCredentials {
177 source: "epo",
178 keys: &[
179 CredentialKey {
180 keychain_key: "epo.consumer_key",
181 env_var: "SCITADEL_EPO_KEY",
182 label: "Consumer key",
183 secret: false,
184 },
185 CredentialKey {
186 keychain_key: "epo.consumer_secret",
187 env_var: "SCITADEL_EPO_SECRET",
188 label: "Consumer secret",
189 secret: true,
190 },
191 ],
192};
193
194pub static ALL_SOURCES: &[&SourceCredentials] = &[
196 &PUBMED_CREDENTIALS,
197 &OPENALEX_CREDENTIALS,
198 &PATENTSVIEW_CREDENTIALS,
199 &LENS_CREDENTIALS,
200 &EPO_CREDENTIALS,
201];
202
203pub fn check_source(creds: &SourceCredentials) -> Result<(), MissingCredential> {
205 let missing: Vec<&CredentialKey> = creds
206 .keys
207 .iter()
208 .filter(|k| resolve(k.keychain_key, k.env_var, "").is_none())
209 .collect();
210
211 if missing.is_empty() {
212 Ok(())
213 } else {
214 Err(MissingCredential {
215 source: creds.source.to_string(),
216 keys: missing.iter().map(|k| k.keychain_key.to_string()).collect(),
217 env_vars: missing.iter().map(|k| k.env_var.to_string()).collect(),
218 remedy: format!("scitadel auth login {}", creds.source),
219 })
220 }
221}