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