1use crate::ux::format::{JsonResponse, Output, is_json_mode};
6use anyhow::{Result, anyhow};
7use auths_core::trust::{PinnedIdentity, PinnedIdentityStore, TrustLevel};
8use chrono::Utc;
9use clap::{Parser, Subcommand};
10use serde::Serialize;
11
12#[derive(Parser, Debug, Clone)]
14#[command(name = "trust", about = "Manage trusted identity roots")]
15pub struct TrustCommand {
16 #[command(subcommand)]
17 pub command: TrustSubcommand,
18}
19
20#[derive(Subcommand, Debug, Clone)]
21pub enum TrustSubcommand {
22 List(TrustListCommand),
24
25 Pin(TrustPinCommand),
27
28 Remove(TrustRemoveCommand),
30
31 Show(TrustShowCommand),
33}
34
35#[derive(Parser, Debug, Clone)]
37pub struct TrustListCommand {}
38
39#[derive(Parser, Debug, Clone)]
41pub struct TrustPinCommand {
42 #[clap(long, required = true)]
44 pub did: String,
45
46 #[clap(long, required = true)]
48 pub key: String,
49
50 #[clap(long)]
52 pub kel_tip: Option<String>,
53
54 #[clap(long)]
56 pub note: Option<String>,
57}
58
59#[derive(Parser, Debug, Clone)]
61pub struct TrustRemoveCommand {
62 pub did: String,
64}
65
66#[derive(Parser, Debug, Clone)]
68pub struct TrustShowCommand {
69 pub did: String,
71}
72
73#[derive(Debug, Serialize)]
75struct TrustActionResult {
76 did: String,
77}
78
79#[derive(Debug, Serialize)]
81struct PinListOutput {
82 pins: Vec<PinSummary>,
83}
84
85#[derive(Debug, Serialize)]
87struct PinSummary {
88 did: String,
89 trust_level: String,
90 first_seen: String,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 kel_sequence: Option<u64>,
93}
94
95#[derive(Debug, Serialize)]
97struct PinDetails {
98 did: String,
99 public_key_hex: String,
100 trust_level: String,
101 first_seen: String,
102 origin: String,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 kel_tip_said: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 kel_sequence: Option<u64>,
107}
108
109pub fn handle_trust(cmd: TrustCommand) -> Result<()> {
111 match cmd.command {
112 TrustSubcommand::List(list_cmd) => handle_list(list_cmd),
113 TrustSubcommand::Pin(pin_cmd) => handle_pin(pin_cmd),
114 TrustSubcommand::Remove(remove_cmd) => handle_remove(remove_cmd),
115 TrustSubcommand::Show(show_cmd) => handle_show(show_cmd),
116 }
117}
118
119fn handle_list(_cmd: TrustListCommand) -> Result<()> {
120 let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
121 let pins = store.list()?;
122
123 if is_json_mode() {
124 JsonResponse::success(
125 "trust list",
126 PinListOutput {
127 pins: pins
128 .iter()
129 .map(|p| PinSummary {
130 did: p.did.clone(),
131 trust_level: format!("{:?}", p.trust_level),
132 first_seen: p.first_seen.to_rfc3339(),
133 kel_sequence: p.kel_sequence,
134 })
135 .collect(),
136 },
137 )
138 .print()?;
139 } else {
140 let out = Output::new();
141 if pins.is_empty() {
142 out.println(&out.dim("No pinned identities."));
143 out.println("");
144 out.println("Use 'auths trust pin --did <DID> --key <HEX>' to pin an identity.");
145 } else {
146 out.println(&format!("{} pinned identities:", pins.len()));
147 out.println("");
148 for pin in &pins {
149 let level = match pin.trust_level {
150 TrustLevel::Tofu => out.dim("TOFU"),
151 TrustLevel::Manual => out.info("Manual"),
152 TrustLevel::OrgPolicy => out.success("OrgPolicy"),
153 };
154 let did_short = truncate_did(&pin.did, 50);
155 out.println(&format!(" {} [{}]", did_short, level));
156 }
157 }
158 }
159
160 Ok(())
161}
162
163fn handle_pin(cmd: TrustPinCommand) -> Result<()> {
164 let bytes = hex::decode(&cmd.key).map_err(|e| anyhow!("Invalid hex for public key: {}", e))?;
166 if bytes.len() != 32 {
167 anyhow::bail!(
168 "Invalid key length: expected 32 bytes (64 hex chars), got {} bytes",
169 bytes.len()
170 );
171 }
172
173 let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
174
175 if let Some(existing) = store.lookup(&cmd.did)? {
177 anyhow::bail!(
178 "Identity {} is already pinned (first seen: {}). Use 'auths trust remove {}' first.",
179 cmd.did,
180 existing.first_seen.format("%Y-%m-%d"),
181 cmd.did
182 );
183 }
184
185 let pin = PinnedIdentity {
186 did: cmd.did.clone(),
187 public_key_hex: cmd.key.clone(),
188 kel_tip_said: cmd.kel_tip,
189 kel_sequence: None,
190 first_seen: Utc::now(),
191 origin: cmd.note.unwrap_or_else(|| "manual".to_string()),
192 trust_level: TrustLevel::Manual,
193 };
194
195 store.pin(pin)?;
196
197 if is_json_mode() {
198 JsonResponse::success(
199 "trust pin",
200 TrustActionResult {
201 did: cmd.did.clone(),
202 },
203 )
204 .print()?;
205 } else {
206 let out = Output::new();
207 out.println(&format!(
208 "{} Pinned identity: {}",
209 out.success("OK"),
210 truncate_did(&cmd.did, 50)
211 ));
212 }
213
214 Ok(())
215}
216
217fn handle_remove(cmd: TrustRemoveCommand) -> Result<()> {
218 let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
219
220 if store.lookup(&cmd.did)?.is_none() {
222 anyhow::bail!("Identity {} is not pinned.", cmd.did);
223 }
224
225 store.remove(&cmd.did)?;
226
227 if is_json_mode() {
228 JsonResponse::success(
229 "trust remove",
230 TrustActionResult {
231 did: cmd.did.clone(),
232 },
233 )
234 .print()?;
235 } else {
236 let out = Output::new();
237 out.println(&format!(
238 "{} Removed pin for: {}",
239 out.success("OK"),
240 truncate_did(&cmd.did, 50)
241 ));
242 }
243
244 Ok(())
245}
246
247fn handle_show(cmd: TrustShowCommand) -> Result<()> {
248 let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
249
250 let pin = store
251 .lookup(&cmd.did)?
252 .ok_or_else(|| anyhow!("Identity {} is not pinned.", cmd.did))?;
253
254 if is_json_mode() {
255 JsonResponse::success(
256 "trust show",
257 PinDetails {
258 did: pin.did.clone(),
259 public_key_hex: pin.public_key_hex.clone(),
260 trust_level: format!("{:?}", pin.trust_level),
261 first_seen: pin.first_seen.to_rfc3339(),
262 origin: pin.origin.clone(),
263 kel_tip_said: pin.kel_tip_said.clone(),
264 kel_sequence: pin.kel_sequence,
265 },
266 )
267 .print()?;
268 } else {
269 let out = Output::new();
270 out.println(&format!("DID: {}", pin.did));
271 out.println(&format!("Public Key: {}", pin.public_key_hex));
272 out.println(&format!("Trust Level: {:?}", pin.trust_level));
273 out.println(&format!(
274 "First Seen: {}",
275 pin.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
276 ));
277 out.println(&format!("Origin: {}", pin.origin));
278 if let Some(ref tip) = pin.kel_tip_said {
279 out.println(&format!("KEL Tip: {}", tip));
280 }
281 if let Some(seq) = pin.kel_sequence {
282 out.println(&format!("KEL Sequence: {}", seq));
283 }
284 }
285
286 Ok(())
287}
288
289fn truncate_did(did: &str, max_len: usize) -> String {
291 if did.len() <= max_len {
292 did.to_string()
293 } else {
294 format!("{}...", &did[..max_len - 3])
295 }
296}
297
298use crate::commands::executable::ExecutableCommand;
299use crate::config::CliConfig;
300
301impl ExecutableCommand for TrustCommand {
302 fn execute(&self, _ctx: &CliConfig) -> Result<()> {
303 handle_trust(self.clone())
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_truncate_did_short() {
313 let did = "did:keri:E123";
314 assert_eq!(truncate_did(did, 20), did);
315 }
316
317 #[test]
318 fn test_truncate_did_long() {
319 let did = "did:keri:EXq5YqaL6L48pf0fu7IUhL0JRaU2_RxFP0AL43wYn148";
320 let truncated = truncate_did(did, 24);
321 assert!(truncated.ends_with("..."));
322 assert_eq!(truncated.len(), 24);
323 }
324}