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 out.println(&format!(" {} [{}]", pin.did, level));
155 }
156 }
157 }
158
159 Ok(())
160}
161
162fn handle_pin(cmd: TrustPinCommand) -> Result<()> {
163 let bytes = hex::decode(&cmd.key).map_err(|e| anyhow!("Invalid hex for public key: {}", e))?;
165 if bytes.len() != 32 {
166 anyhow::bail!(
167 "Invalid key length: expected 32 bytes (64 hex chars), got {} bytes",
168 bytes.len()
169 );
170 }
171
172 let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
173
174 if let Some(existing) = store.lookup(&cmd.did)? {
176 anyhow::bail!(
177 "Identity {} is already pinned (first seen: {}). Use 'auths trust remove {}' first.",
178 cmd.did,
179 existing.first_seen.format("%Y-%m-%d"),
180 cmd.did
181 );
182 }
183
184 let pin = PinnedIdentity {
185 did: cmd.did.clone(),
186 public_key_hex: cmd.key.clone(),
187 kel_tip_said: cmd.kel_tip,
188 kel_sequence: None,
189 first_seen: Utc::now(),
190 origin: cmd.note.unwrap_or_else(|| "manual".to_string()),
191 trust_level: TrustLevel::Manual,
192 };
193
194 store.pin(pin)?;
195
196 if is_json_mode() {
197 JsonResponse::success(
198 "trust pin",
199 TrustActionResult {
200 did: cmd.did.clone(),
201 },
202 )
203 .print()?;
204 } else {
205 let out = Output::new();
206 out.println(&format!(
207 "{} Pinned identity: {}",
208 out.success("OK"),
209 &cmd.did
210 ));
211 }
212
213 Ok(())
214}
215
216fn handle_remove(cmd: TrustRemoveCommand) -> Result<()> {
217 let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
218
219 if store.lookup(&cmd.did)?.is_none() {
221 anyhow::bail!("Identity {} is not pinned.", cmd.did);
222 }
223
224 store.remove(&cmd.did)?;
225
226 if is_json_mode() {
227 JsonResponse::success(
228 "trust remove",
229 TrustActionResult {
230 did: cmd.did.clone(),
231 },
232 )
233 .print()?;
234 } else {
235 let out = Output::new();
236 out.println(&format!(
237 "{} Removed pin for: {}",
238 out.success("OK"),
239 &cmd.did
240 ));
241 }
242
243 Ok(())
244}
245
246fn handle_show(cmd: TrustShowCommand) -> Result<()> {
247 let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
248
249 let pin = store
250 .lookup(&cmd.did)?
251 .ok_or_else(|| anyhow!("Identity {} is not pinned.", cmd.did))?;
252
253 if is_json_mode() {
254 JsonResponse::success(
255 "trust show",
256 PinDetails {
257 did: pin.did.clone(),
258 public_key_hex: pin.public_key_hex.clone(),
259 trust_level: format!("{:?}", pin.trust_level),
260 first_seen: pin.first_seen.to_rfc3339(),
261 origin: pin.origin.clone(),
262 kel_tip_said: pin.kel_tip_said.clone(),
263 kel_sequence: pin.kel_sequence,
264 },
265 )
266 .print()?;
267 } else {
268 let out = Output::new();
269 out.println(&format!("DID: {}", pin.did));
270 out.println(&format!("Public Key: {}", pin.public_key_hex));
271 out.println(&format!("Trust Level: {:?}", pin.trust_level));
272 out.println(&format!(
273 "First Seen: {}",
274 pin.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
275 ));
276 out.println(&format!("Origin: {}", pin.origin));
277 if let Some(ref tip) = pin.kel_tip_said {
278 out.println(&format!("KEL Tip: {}", tip));
279 }
280 if let Some(seq) = pin.kel_sequence {
281 out.println(&format!("KEL Sequence: {}", seq));
282 }
283 }
284
285 Ok(())
286}
287
288use crate::commands::executable::ExecutableCommand;
289use crate::config::CliConfig;
290
291impl ExecutableCommand for TrustCommand {
292 fn execute(&self, _ctx: &CliConfig) -> Result<()> {
293 handle_trust(self.clone())
294 }
295}