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