auths_cli/errors/
renderer.rs1use anyhow::Error;
2use auths_core::error::{AgentError, AuthsErrorInfo as CoreErrorInfo};
3use auths_sdk::error::{DeviceError, SetupError};
4use auths_sdk::signing::SigningError;
5use auths_verifier::{AttestationError, AuthsErrorInfo as VerifierErrorInfo};
6use colored::Colorize;
7
8use crate::errors::cli_error::CliError;
9use crate::ux::format::Output;
10
11const DOCS_BASE_URL: &str = "https://docs.auths.dev";
12
13pub fn render_error(err: &Error, json_mode: bool) {
20 if json_mode {
21 render_json(err);
22 } else {
23 render_text(err);
24 }
25}
26
27fn render_text(err: &Error) {
29 let out = Output::new();
30
31 if let Some(cli_err) = err.downcast_ref::<CliError>() {
33 eprintln!("\n{} {}", "Error:".red().bold(), cli_err);
34 eprintln!("\n{}", cli_err.suggestion());
35 if let Some(url) = cli_err.docs_url() {
36 eprintln!("Docs: {}", url);
37 }
38 eprintln!();
39 return;
40 }
41
42 if let Some(signing_err) = err.downcast_ref::<SigningError>() {
43 let message = format!("{signing_err}");
44 out.print_error(&out.bold(&message));
45 eprintln!();
46 let suggestion = match signing_err {
47 SigningError::PassphraseExhausted { attempts } => Some(format!(
48 "All {attempts} passphrase attempt(s) failed.\n Forgot your passphrase? Run: auths key reset <alias>"
49 )),
50 SigningError::IdentityFrozen(_) => {
51 Some("To unfreeze: auths emergency unfreeze".to_string())
52 }
53 SigningError::KeychainUnavailable(_) => Some(format!(
54 "Cannot access system keychain.\n\n If running headless (CI/Docker), set:\n export AUTHS_KEYCHAIN_BACKEND=file\n export AUTHS_PASSPHRASE=<your-passphrase>\n\n See: {DOCS_BASE_URL}/cli/troubleshooting/"
55 )),
56 _ => None,
57 };
58 if let Some(suggestion) = suggestion {
59 eprintln!(" fix: {suggestion}");
60 }
61 return;
62 }
63
64 if let Some(agent_err) = err.downcast_ref::<AgentError>() {
65 let code = CoreErrorInfo::error_code(agent_err);
66 let message = format!("{agent_err}");
67 out.print_error(&out.bold(&message));
68 eprintln!();
69 if let Some(suggestion) = CoreErrorInfo::suggestion(agent_err) {
70 eprintln!(" fix: {suggestion}");
71 }
72 if let Some(url) = docs_url(code) {
73 eprintln!(" docs: {url}");
74 }
75 } else if let Some(att_err) = err.downcast_ref::<AttestationError>() {
76 let code = VerifierErrorInfo::error_code(att_err);
77 let message = format!("{att_err}");
78 out.print_error(&out.bold(&message));
79 eprintln!();
80 if let Some(suggestion) = VerifierErrorInfo::suggestion(att_err) {
81 eprintln!(" fix: {suggestion}");
82 }
83 if let Some(url) = docs_url(code) {
84 eprintln!(" docs: {url}");
85 }
86 } else if let Some(setup_err) = err.downcast_ref::<SetupError>() {
87 let code = CoreErrorInfo::error_code(setup_err);
88 let message = format!("{setup_err}");
89 out.print_error(&out.bold(&message));
90 eprintln!();
91 if let Some(suggestion) = CoreErrorInfo::suggestion(setup_err) {
92 eprintln!(" fix: {suggestion}");
93 }
94 if let Some(url) = docs_url(code) {
95 eprintln!(" docs: {url}");
96 }
97 } else if let Some(device_err) = err.downcast_ref::<DeviceError>() {
98 let code = CoreErrorInfo::error_code(device_err);
99 let message = format!("{device_err}");
100 out.print_error(&out.bold(&message));
101 eprintln!();
102 if let Some(suggestion) = CoreErrorInfo::suggestion(device_err) {
103 eprintln!(" fix: {suggestion}");
104 }
105 if let Some(url) = docs_url(code) {
106 eprintln!(" docs: {url}");
107 }
108 } else {
109 let msg = err.to_string();
111 let suggestion = match msg.as_str() {
112 s if s.contains("No identity found") => Some(format!(
113 "Run `auths init` to create one, or `auths key import` to restore from a backup.\n See: {DOCS_BASE_URL}/getting-started/quickstart/"
114 )),
115 s if s.contains("keychain") || s.contains("Secret Service") => Some(format!(
116 "Cannot access system keychain.\n\n If running headless (CI/Docker), set:\n export AUTHS_KEYCHAIN_BACKEND=file\n export AUTHS_PASSPHRASE=<your-passphrase>\n\n See: {DOCS_BASE_URL}/cli/troubleshooting/"
117 )),
118 s if s.contains("ssh-keygen") && s.contains("not found") => Some(
119 "ssh-keygen not found on PATH.\n\n Install OpenSSH:\n Ubuntu: sudo apt install openssh-client\n macOS: ssh-keygen is pre-installed\n Windows: Install OpenSSH via Settings > Apps > Optional features".to_string()
120 ),
121 _ => None,
122 };
123
124 if let Some(suggestion) = suggestion {
125 out.print_error(&msg);
126 eprintln!("\n{suggestion}");
127 } else {
128 out.print_error(&format!("{err}"));
130 for cause in err.chain().skip(1) {
131 eprintln!(" caused by: {cause}");
132 }
133 }
134 }
135}
136
137fn render_json(err: &Error) {
139 let json = if let Some(cli_err) = err.downcast_ref::<CliError>() {
140 build_json(
141 None,
142 &format!("{cli_err}"),
143 Some(cli_err.suggestion()),
144 cli_err.docs_url().map(|s| s.to_string()),
145 )
146 } else if let Some(signing_err) = err.downcast_ref::<SigningError>() {
147 let suggestion = match signing_err {
148 SigningError::PassphraseExhausted { attempts } => Some(format!(
149 "All {} passphrase attempt(s) failed. Run: auths key reset <alias>",
150 attempts
151 )),
152 SigningError::IdentityFrozen(_) => {
153 Some("To unfreeze: auths emergency unfreeze".to_string())
154 }
155 _ => None,
156 };
157 build_json(None, &format!("{signing_err}"), suggestion.as_deref(), None)
158 } else if let Some(agent_err) = err.downcast_ref::<AgentError>() {
159 let code = CoreErrorInfo::error_code(agent_err);
160 build_json(
161 Some(code),
162 &format!("{agent_err}"),
163 CoreErrorInfo::suggestion(agent_err),
164 docs_url(code),
165 )
166 } else if let Some(att_err) = err.downcast_ref::<AttestationError>() {
167 let code = VerifierErrorInfo::error_code(att_err);
168 build_json(
169 Some(code),
170 &format!("{att_err}"),
171 VerifierErrorInfo::suggestion(att_err),
172 docs_url(code),
173 )
174 } else if let Some(setup_err) = err.downcast_ref::<SetupError>() {
175 let code = CoreErrorInfo::error_code(setup_err);
176 build_json(
177 Some(code),
178 &format!("{setup_err}"),
179 CoreErrorInfo::suggestion(setup_err),
180 docs_url(code),
181 )
182 } else if let Some(device_err) = err.downcast_ref::<DeviceError>() {
183 let code = CoreErrorInfo::error_code(device_err);
184 build_json(
185 Some(code),
186 &format!("{device_err}"),
187 CoreErrorInfo::suggestion(device_err),
188 docs_url(code),
189 )
190 } else {
191 build_json(None, &format!("{err}"), None, None)
192 };
193
194 eprintln!("{json}");
195}
196
197fn build_json(
199 code: Option<&str>,
200 message: &str,
201 suggestion: Option<&str>,
202 docs: Option<String>,
203) -> String {
204 let mut map = serde_json::Map::new();
205 if let Some(c) = code {
206 map.insert("code".into(), serde_json::Value::String(c.into()));
207 }
208 map.insert("message".into(), serde_json::Value::String(message.into()));
209 if let Some(s) = suggestion {
210 map.insert("suggestion".into(), serde_json::Value::String(s.into()));
211 }
212 if let Some(d) = docs {
213 map.insert("docs".into(), serde_json::Value::String(d));
214 }
215
216 let wrapper = serde_json::json!({ "error": map });
217 serde_json::to_string_pretty(&wrapper)
218 .unwrap_or_else(|_| format!("{{\"error\":{{\"message\":\"{message}\"}}}}"))
219}
220
221fn docs_url(code: &str) -> Option<String> {
224 match code {
225 "AUTHS_KEY_NOT_FOUND"
226 | "AUTHS_INCORRECT_PASSPHRASE"
227 | "AUTHS_MISSING_PASSPHRASE"
228 | "AUTHS_BACKEND_UNAVAILABLE"
229 | "AUTHS_STORAGE_LOCKED"
230 | "AUTHS_BACKEND_INIT_FAILED"
231 | "AUTHS_AGENT_LOCKED"
232 | "AUTHS_ISSUER_SIG_FAILED"
233 | "AUTHS_DEVICE_SIG_FAILED"
234 | "AUTHS_ATTESTATION_EXPIRED"
235 | "AUTHS_ATTESTATION_REVOKED"
236 | "AUTHS_TIMESTAMP_IN_FUTURE"
237 | "AUTHS_MISSING_CAPABILITY"
238 | "AUTHS_DID_RESOLUTION_ERROR"
239 | "AUTHS_ORG_VERIFICATION_FAILED"
240 | "AUTHS_ORG_ATTESTATION_EXPIRED"
241 | "AUTHS_ORG_DID_RESOLUTION_FAILED"
242 | "AUTHS_GIT_ERROR" => Some(format!("{DOCS_BASE_URL}/errors/#{code}")),
243 _ => None,
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn docs_url_returns_some_for_known_codes() {
253 let url = docs_url("AUTHS_KEY_NOT_FOUND");
254 assert_eq!(
255 url,
256 Some(format!("{DOCS_BASE_URL}/errors/#AUTHS_KEY_NOT_FOUND"))
257 );
258 }
259
260 #[test]
261 fn docs_url_returns_none_for_unknown_codes() {
262 assert!(docs_url("AUTHS_IO_ERROR").is_none());
263 assert!(docs_url("UNKNOWN").is_none());
264 }
265
266 #[test]
267 fn build_json_with_all_fields() {
268 let json = build_json(
269 Some("AUTHS_KEY_NOT_FOUND"),
270 "Key not found",
271 Some("Run `auths key list`"),
272 Some(format!("{DOCS_BASE_URL}/errors/#AUTHS_KEY_NOT_FOUND")),
273 );
274 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
275 assert_eq!(parsed["error"]["code"], "AUTHS_KEY_NOT_FOUND");
276 assert_eq!(parsed["error"]["message"], "Key not found");
277 assert_eq!(parsed["error"]["suggestion"], "Run `auths key list`");
278 assert!(
279 parsed["error"]["docs"]
280 .as_str()
281 .unwrap()
282 .contains("AUTHS_KEY_NOT_FOUND")
283 );
284 }
285
286 #[test]
287 fn build_json_without_optional_fields() {
288 let json = build_json(None, "Something went wrong", None, None);
289 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
290 assert_eq!(parsed["error"]["message"], "Something went wrong");
291 assert!(parsed["error"].get("code").is_none());
292 assert!(parsed["error"].get("suggestion").is_none());
293 assert!(parsed["error"].get("docs").is_none());
294 }
295
296 #[test]
297 fn render_error_agent_error_text() {
298 let err = Error::new(AgentError::KeyNotFound);
299 render_error(&err, false);
301 }
302
303 #[test]
304 fn render_error_agent_error_json() {
305 let err = Error::new(AgentError::KeyNotFound);
306 render_error(&err, true);
307 }
308
309 #[test]
310 fn render_error_attestation_error_text() {
311 let err = Error::new(AttestationError::IssuerSignatureFailed("bad sig".into()));
312 render_error(&err, false);
313 }
314
315 #[test]
316 fn render_error_unknown_error_text() {
317 let err = anyhow::anyhow!("something unexpected");
318 render_error(&err, false);
319 }
320
321 #[test]
322 fn render_error_unknown_error_json() {
323 let err = anyhow::anyhow!("something unexpected");
324 render_error(&err, true);
325 }
326}