use clap::Subcommand;
use serde_json::Value;
use vta_cli_common::render::{DIM, GREEN, RESET, is_json_output, print_json};
use vta_sdk::session::TransportChoice;
use vtc_client::{RotationReason, VtcClient, VtcError};
use crate::vtc::{self as vtc_target, VtcTarget};
type CliResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
#[derive(Subcommand)]
pub enum MemberCommands {
Renew,
Rotate {
#[command(subcommand)]
command: RotateCommands,
},
RevokePersonhood {
did: String,
},
Relationships {
#[command(subcommand)]
command: RelationshipCommands,
},
Endorse {
subject: String,
#[arg(long = "type")]
type_uri: String,
#[arg(long)]
claim: String,
#[arg(long)]
valid_for: Option<String>,
},
}
#[derive(Subcommand)]
pub enum RotateCommands {
Challenge {
#[arg(long)]
reason: Option<String>,
},
Finish {
#[arg(long)]
rotation_id: String,
#[arg(long)]
old_did: String,
#[arg(long)]
new_did: String,
#[arg(long)]
old_signature: String,
#[arg(long)]
new_signature: String,
},
}
#[derive(Subcommand)]
pub enum RelationshipCommands {
List {
did: String,
},
Publish {
vrc_file: String,
#[arg(long)]
pop_file: Option<String>,
},
Revoke {
id: String,
#[arg(long)]
pop_file: Option<String>,
},
}
pub async fn run(
command: MemberCommands,
keyring_key: &str,
target: &VtcTarget,
transport: TransportChoice,
) -> CliResult {
let vtc = vtc_target::connect_for_tasks(keyring_key, target, transport).await?;
let outcome = run_command(command, &vtc.client).await;
vtc.client.shutdown().await;
outcome
}
async fn run_command(command: MemberCommands, vtc: &VtcClient) -> CliResult {
match command {
MemberCommands::Renew => {
let renewed = vtc.renew().await.map_err(member_error)?;
report(&serde_json::to_value(&renewed)?, "renewed")
}
MemberCommands::Rotate { command } => run_rotate(command, vtc).await,
MemberCommands::RevokePersonhood { did } => {
let revoked = vtc.revoke_personhood(&did).await.map_err(member_error)?;
report(&serde_json::to_value(&revoked)?, "personhood revoked")
}
MemberCommands::Relationships { command } => run_relationships(command, vtc).await,
MemberCommands::Endorse {
subject,
type_uri,
claim,
valid_for,
} => {
let claim: Value = serde_json::from_str(&claim)
.map_err(|e| format!("--claim is not valid JSON: {e}"))?;
let valid_for_seconds = valid_for
.as_deref()
.map(vta_cli_common::duration::parse_duration_secs)
.transpose()?;
let issued = vtc
.issue_endorsement(&subject, &type_uri, claim, valid_for_seconds)
.await
.map_err(member_error)?;
report(&serde_json::to_value(&issued)?, "issued")
}
}
}
async fn run_rotate(command: RotateCommands, vtc: &VtcClient) -> CliResult {
match command {
RotateCommands::Challenge { reason } => {
let reason = reason.as_deref().map(parse_reason).transpose()?;
let challenge = vtc.rotate_challenge(reason).await.map_err(member_error)?;
report(
&serde_json::to_value(&challenge)?,
"rotation challenge opened",
)
}
RotateCommands::Finish {
rotation_id,
old_did,
new_did,
old_signature,
new_signature,
} => {
let rotated = vtc
.rotate(
&rotation_id,
&old_did,
&new_did,
&old_signature,
&new_signature,
)
.await
.map_err(member_error)?;
report(&serde_json::to_value(&rotated)?, "rotated")
}
}
}
async fn run_relationships(command: RelationshipCommands, vtc: &VtcClient) -> CliResult {
match command {
RelationshipCommands::List { did } => {
let items = vtc.list_relationships(&did).await.map_err(member_error)?;
if is_json_output() {
print_json(&items)?;
} else if items.is_empty() {
println!("{DIM}no relationships.{RESET}");
} else {
for r in &items {
println!(
"{} {} -> {} {DIM}({}){RESET}",
r.id, r.issuer_did, r.subject_did, r.created_at
);
}
}
Ok(())
}
RelationshipCommands::Publish { vrc_file, pop_file } => {
let vrc: Value = serde_json::from_str(&std::fs::read_to_string(&vrc_file)?)?;
let pop = pop_file
.map(|p| -> CliResult<Value> {
Ok(serde_json::from_str(&std::fs::read_to_string(&p)?)?)
})
.transpose()?;
let published = vtc
.publish_relationship(vrc, pop)
.await
.map_err(member_error)?;
report(&serde_json::to_value(&published)?, "published")
}
RelationshipCommands::Revoke { id, pop_file } => {
let pop = pop_file
.map(|p| -> CliResult<Value> {
Ok(serde_json::from_str(&std::fs::read_to_string(&p)?)?)
})
.transpose()?;
let revoked = vtc
.revoke_relationship(&id, pop)
.await
.map_err(member_error)?;
report(&serde_json::to_value(&revoked)?, "revoked")
}
}
}
fn parse_reason(s: &str) -> CliResult<RotationReason> {
Ok(match s {
"routine" => RotationReason::Routine,
"compromise" => RotationReason::Compromise,
"device-loss" | "deviceLoss" => RotationReason::DeviceLoss,
"migration" => RotationReason::Migration,
"unspecified" => RotationReason::Unspecified,
other => {
return Err(format!(
"--reason must be one of routine, compromise, device-loss, migration, \
unspecified (got `{other}`)"
)
.into());
}
})
}
fn report(value: &Value, verb: &str) -> CliResult {
if is_json_output() {
print_json(value)?;
} else {
println!("{GREEN}{verb}{RESET}");
println!("{}", serde_json::to_string_pretty(value)?);
}
Ok(())
}
fn member_error(err: VtcError) -> Box<dyn std::error::Error> {
err.to_string().into()
}