auths_cli/commands/
key_state.rs1use std::path::{Path, PathBuf};
11
12use anyhow::{Result, anyhow};
13use auths_keri::{KeyStateRecord, TrustedKel, parse_kel_json};
14use auths_utils::path::expand_tilde;
15use clap::Parser;
16
17use crate::config::CliConfig;
18
19#[derive(Parser, Debug, Clone)]
21#[command(
22 about = "Emit or ingest a KERI key-state notice (ksn) — interoperable with keripy/keriox",
23 visible_alias = "ksn",
24 after_help = "Examples:
25 auths key-state --from-kel kel.json # emit a KERI ksn a peer can read
26 auths key-state --ingest ksn.json # consume a keripy/keriox ksn"
27)]
28pub struct KeyStateCommand {
29 #[clap(long, value_name = "KEL.json")]
32 pub from_kel: Option<PathBuf>,
33
34 #[clap(long, value_name = "KSN.json", conflicts_with = "from_kel")]
37 pub ingest: Option<PathBuf>,
38
39 #[clap(long, value_name = "HEX_SEQ", requires = "ingest")]
44 pub reject_stale_below: Option<String>,
45
46 #[clap(long, default_value = "1970-01-01T00:00:00+00:00")]
49 pub dt: String,
50}
51
52impl KeyStateCommand {
53 pub fn execute(&self, _ctx: &CliConfig) -> Result<()> {
55 match (&self.from_kel, &self.ingest) {
56 (Some(kel_path), None) => self.emit(kel_path),
57 (None, Some(ksn_path)) => self.ingest_record(ksn_path),
58 (Some(_), Some(_)) => {
59 unreachable!("clap conflicts_with guarantees mutual exclusion")
60 }
61 (None, None) => Err(anyhow!(
62 "key-state needs --from-kel <KEL.json> (emit) or --ingest <KSN.json> (consume)"
63 )),
64 }
65 }
66
67 fn emit(&self, kel_path: &Path) -> Result<()> {
69 let path = expand_tilde(kel_path)?;
70 let json = std::fs::read_to_string(&path)
71 .map_err(|e| anyhow!("read KEL {}: {e}", path.display()))?;
72 let events = parse_kel_json(&json).map_err(|e| anyhow!("parse KEL: {e}"))?;
73 let state = TrustedKel::from_trusted_source(&events)
76 .replay()
77 .map_err(|e| anyhow!("replay KEL: {e}"))?;
78 let record = KeyStateRecord::from_kel(&events, &state, self.dt.clone())
79 .ok_or_else(|| anyhow!("KEL is empty — no key-state to notice"))?;
80 println!("{}", serde_json::to_string_pretty(&record)?);
81 Ok(())
82 }
83
84 fn ingest_record(&self, ksn_path: &Path) -> Result<()> {
86 let path = expand_tilde(ksn_path)?;
87 let json = std::fs::read_to_string(&path)
88 .map_err(|e| anyhow!("read ksn {}: {e}", path.display()))?;
89 let record: KeyStateRecord =
90 serde_json::from_str(&json).map_err(|e| anyhow!("parse KERI ksn: {e}"))?;
91
92 if let Some(hex_seq) = &self.reject_stale_below {
95 let last_seen =
96 u128::from_str_radix(hex_seq.trim_start_matches("0x"), 16).map_err(|_| {
97 anyhow!("--reject-stale-below must be lowercase hex, got {hex_seq:?}")
98 })?;
99 record.check_not_stale(last_seen).map_err(|e| {
100 anyhow!("rejected: stale key-state notice — {e}; a verifier holding a newer state refuses to rewind")
101 })?;
102 }
103
104 let state = record.into_key_state();
105 println!("{}", serde_json::to_string_pretty(&state)?);
106 Ok(())
107 }
108}