use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use auths_keri::{KeyStateRecord, TrustedKel, parse_kel_json};
use auths_utils::path::expand_tilde;
use clap::Parser;
use crate::config::CliConfig;
#[derive(Parser, Debug, Clone)]
#[command(
about = "Emit or ingest a KERI key-state notice (ksn) — interoperable with keripy/keriox",
visible_alias = "ksn",
after_help = "Examples:
auths key-state --from-kel kel.json # emit a KERI ksn a peer can read
auths key-state --ingest ksn.json # consume a keripy/keriox ksn"
)]
pub struct KeyStateCommand {
#[clap(long, value_name = "KEL.json")]
pub from_kel: Option<PathBuf>,
#[clap(long, value_name = "KSN.json", conflicts_with = "from_kel")]
pub ingest: Option<PathBuf>,
#[clap(long, value_name = "HEX_SEQ", requires = "ingest")]
pub reject_stale_below: Option<String>,
#[clap(long, default_value = "1970-01-01T00:00:00+00:00")]
pub dt: String,
}
impl KeyStateCommand {
pub fn execute(&self, _ctx: &CliConfig) -> Result<()> {
match (&self.from_kel, &self.ingest) {
(Some(kel_path), None) => self.emit(kel_path),
(None, Some(ksn_path)) => self.ingest_record(ksn_path),
(Some(_), Some(_)) => {
unreachable!("clap conflicts_with guarantees mutual exclusion")
}
(None, None) => Err(anyhow!(
"key-state needs --from-kel <KEL.json> (emit) or --ingest <KSN.json> (consume)"
)),
}
}
fn emit(&self, kel_path: &Path) -> Result<()> {
let path = expand_tilde(kel_path)?;
let json = std::fs::read_to_string(&path)
.map_err(|e| anyhow!("read KEL {}: {e}", path.display()))?;
let events = parse_kel_json(&json).map_err(|e| anyhow!("parse KEL: {e}"))?;
let state = TrustedKel::from_trusted_source(&events)
.replay()
.map_err(|e| anyhow!("replay KEL: {e}"))?;
let record = KeyStateRecord::from_kel(&events, &state, self.dt.clone())
.ok_or_else(|| anyhow!("KEL is empty — no key-state to notice"))?;
println!("{}", serde_json::to_string_pretty(&record)?);
Ok(())
}
fn ingest_record(&self, ksn_path: &Path) -> Result<()> {
let path = expand_tilde(ksn_path)?;
let json = std::fs::read_to_string(&path)
.map_err(|e| anyhow!("read ksn {}: {e}", path.display()))?;
let record: KeyStateRecord =
serde_json::from_str(&json).map_err(|e| anyhow!("parse KERI ksn: {e}"))?;
if let Some(hex_seq) = &self.reject_stale_below {
let last_seen =
u128::from_str_radix(hex_seq.trim_start_matches("0x"), 16).map_err(|_| {
anyhow!("--reject-stale-below must be lowercase hex, got {hex_seq:?}")
})?;
record.check_not_stale(last_seen).map_err(|e| {
anyhow!("rejected: stale key-state notice — {e}; a verifier holding a newer state refuses to rewind")
})?;
}
let state = record.into_key_state();
println!("{}", serde_json::to_string_pretty(&state)?);
Ok(())
}
}