use serde_json::Value;
use vta_sdk::client::VtaClient;
use vta_sdk::protocols::persona::{
ContactDocument, LocalProfileEntry, ProfileEntry, Provenance, ValueType,
};
use crate::render::{BOLD, CYAN, DIM, RED, RESET, YELLOW, is_json_output, print_json};
type CmdResult = Result<(), Box<dyn std::error::Error>>;
fn print_result(label: &str, value: &Value) -> CmdResult {
if is_json_output() {
print_json(value)?;
} else {
println!("{label}");
println!("{}", serde_json::to_string_pretty(value)?);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn cmd_attribute_put(
client: &VtaClient,
claim_type: String,
value: Value,
value_type: ValueType,
provenance: Provenance,
label: Option<String>,
endorsements: Vec<String>,
attribute_id: Option<String>,
expected_version: Option<u64>,
) -> CmdResult {
let result = client
.persona_attribute_put(
&claim_type,
value,
value_type,
provenance,
label.as_deref(),
endorsements,
attribute_id.as_deref(),
expected_version,
)
.await?;
print_result("Attribute:", &result)
}
pub async fn cmd_attribute_list(
client: &VtaClient,
type_prefix: Option<String>,
include_values: bool,
include_sensitive: bool,
include_stale: Option<bool>,
limit: Option<std::num::NonZeroU64>,
cursor: Option<String>,
) -> CmdResult {
let result = client
.persona_attribute_list(
type_prefix.as_deref(),
include_values,
include_sensitive,
include_stale,
limit,
cursor.as_deref(),
)
.await?;
if !is_json_output() {
if !include_values {
println!("{DIM}Names only — add --values to include the values themselves.{RESET}");
} else if !include_sensitive {
println!(
"{DIM}Sensitive values — cards, passports, phone numbers — are held back. \
Add --sensitive to include them.{RESET}"
);
}
}
print_result("Your attributes:", &result)
}
pub async fn cmd_attribute_delete(
client: &VtaClient,
attribute_id: String,
cascade: bool,
expected_version: Option<u64>,
) -> CmdResult {
let result = client
.persona_attribute_delete(&attribute_id, cascade, expected_version)
.await?;
if !is_json_output() && cascade {
println!(
"{DIM}Cascaded: {attribute_id} was taken off every face that showed it. Nothing \
already shared is affected — that has left.{RESET}"
);
}
print_result("Result:", &result)
}
pub async fn cmd_attribute_promote(
client: &VtaClient,
context: String,
profile_id: String,
entries: Vec<u64>,
expected_version: u64,
) -> CmdResult {
let out = client
.persona_attribute_promote(&context, &profile_id, &entries, expected_version)
.await?;
let result = serde_json::to_value(&out)?;
if !is_json_output() {
println!(
"{DIM}{profile_id} is now a face in your pool — same id, still worn by {} persona(s). \
The promoted values can be shown by your other faces; this cannot be undone.{RESET}",
out.rebound_persona_dids.len()
);
}
print_result("Promoted:", &result)
}
pub async fn cmd_attribute_purge_version(
client: &VtaClient,
attribute_id: String,
versions: Vec<u64>,
) -> CmdResult {
let out = client
.persona_attribute_purge_version(
&attribute_id,
(!versions.is_empty()).then_some(versions.as_slice()),
)
.await?;
let result = serde_json::to_value(&out)?;
if !is_json_output() {
let stale = out.stale_pins.len();
if out.purged.is_empty() {
println!("{DIM}Nothing to remove — no kept version of {attribute_id} matched.{RESET}");
} else if stale > 0 {
println!(
"{YELLOW}{stale} face(s) pinned what was removed, and now show nothing for it. \
Repin or edit them if they should show something.{RESET}"
);
}
println!(
"{DIM}Removed from your agent. Anyone already shown the old value keeps it — \
`persona disclosure history` says who.{RESET}"
);
}
print_result("Result:", &result)
}
pub async fn cmd_profile_put(
client: &VtaClient,
name: String,
entries: Vec<ProfileEntry>,
credential_refs: Vec<String>,
reach: Option<trust_tasks_rs::specs::persona::profile::put::v1_0::FaceReach>,
profile_id: Option<String>,
expected_version: Option<u64>,
) -> CmdResult {
let result = client
.persona_profile_put(
&name,
entries,
credential_refs,
reach,
profile_id.as_deref(),
expected_version,
)
.await?;
print_result("Face:", &result)
}
pub async fn cmd_profile_compose(
client: &VtaClient,
context: String,
name: String,
claims: Vec<Value>,
persona_did: Option<String>,
label: Option<String>,
until: Option<String>,
) -> CmdResult {
let mut body = serde_json::json!({ "contextId": context, "name": name, "claims": claims });
if let Some(d) = persona_did {
body["personaDid"] = d.into();
}
if let Some(l) = label {
body["label"] = l.into();
}
if let Some(u) = until {
body["until"] = u.into();
}
let payload: trust_tasks_rs::specs::persona::profile::compose::v1_0::Payload =
serde_json::from_value(body).map_err(|e| format!("not a valid compose: {e}"))?;
let out = client.persona_profile_compose(&payload).await?;
let result = serde_json::to_value(&out)?;
if !is_json_output() {
let reused = out.pooled.iter().filter(|p| !p.created).count();
match out.scope.to_string().as_str() {
"local" => println!(
"{DIM}Kept in this context: nothing you typed is reusable elsewhere. `persona \
attribute promote` makes a value reusable later.{RESET}"
),
_ => println!(
"{DIM}A face in your pool, so it can be worn in other contexts too.{RESET}"
),
}
if reused > 0 {
println!(
"{YELLOW}{reused} shared value(s) were already kept, so this face now draws on the \
same fact as whatever else shows it — an edit changes both.{RESET}"
);
}
}
print_result("Face:", &result)
}
pub async fn cmd_profile_get(client: &VtaClient, profile_id: String, resolve: bool) -> CmdResult {
let result = client.persona_profile_get(&profile_id, resolve).await?;
if !is_json_output() && !resolve {
println!(
"{DIM}Showing how the face is built — add --resolve to see the attributes it would \
show.{RESET}"
);
}
print_result("Face:", &result)
}
pub async fn cmd_profile_list(
client: &VtaClient,
limit: Option<std::num::NonZeroU64>,
cursor: Option<String>,
include_retired: bool,
) -> CmdResult {
let result = client
.persona_profile_list(limit, cursor.as_deref(), include_retired)
.await?;
print_result("Faces:", &result)
}
pub async fn cmd_profile_usage(
client: &VtaClient,
profile_id: String,
context: Option<String>,
) -> CmdResult {
let out = client
.persona_profile_usage(&profile_id, context.as_deref())
.await?;
let result = serde_json::to_value(&out)?;
if !is_json_output() && out.usage.is_empty() {
println!("{DIM}Worn nowhere.{RESET}");
}
print_result("Worn in:", &result)
}
pub async fn cmd_profile_timeline(
client: &VtaClient,
profile_id: String,
context: Option<String>,
since: Option<String>,
cursor: Option<String>,
limit: Option<u64>,
) -> CmdResult {
let out = client
.persona_profile_timeline(
&profile_id,
context.as_deref(),
since.as_deref(),
cursor.as_deref(),
limit,
)
.await?;
let result = serde_json::to_value(&out)?;
if !is_json_output() {
println!(
"{DIM}Types and parties only — a timeline never holds a value or a name you gave \
something.{RESET}"
);
if let Some(next) = &out.next_cursor {
println!("{DIM}More follows: --cursor {}{RESET}", next.as_str());
}
}
print_result("Timeline:", &result)
}
pub async fn cmd_profile_retire(
client: &VtaClient,
profile_id: String,
context: Option<String>,
expected_version: Option<u64>,
) -> CmdResult {
let out = client
.persona_profile_retire(&profile_id, context.as_deref(), expected_version)
.await?;
let result = serde_json::to_value(&out)?;
if !is_json_output() {
println!(
"{DIM}Retired: taken off {} place(s) it was worn, and kept. `persona profile \
reinstate` makes it wearable again.{RESET}",
out.unbound.len()
);
}
print_result("Retired:", &result)
}
pub async fn cmd_profile_reinstate(
client: &VtaClient,
profile_id: String,
context: Option<String>,
expected_version: Option<u64>,
) -> CmdResult {
let out = client
.persona_profile_reinstate(&profile_id, context.as_deref(), expected_version)
.await?;
let result = serde_json::to_value(&out)?;
if !is_json_output() {
println!("{DIM}Wearable again, and worn nowhere until you wear it somewhere.{RESET}");
}
print_result("Reinstated:", &result)
}
pub async fn cmd_profile_delete(
client: &VtaClient,
profile_id: String,
unbind: bool,
expected_version: Option<u64>,
) -> CmdResult {
let result = client
.persona_profile_delete(&profile_id, unbind, expected_version)
.await?;
if !is_json_output() && unbind {
println!(
"{YELLOW}Taken off: every persona wearing {profile_id} now shows nothing until \
it wears another. Nothing already shared is affected — that has left.{RESET}"
);
}
if !is_json_output()
&& let Some(parties) = result["disclosedTo"]["partyCount"].as_u64()
&& parties > 0
{
println!(
"{YELLOW}This face disclosed to {parties} part{} across {} context(s). Deleting it \
does not un-tell them.{RESET}",
if parties == 1 { "y" } else { "ies" },
result["disclosedTo"]["contextCount"].as_u64().unwrap_or(0),
);
}
print_result("Result:", &result)
}
pub async fn cmd_binding_set(
client: &VtaClient,
context_id: String,
persona_did: String,
profile_id: Option<String>,
public_entries: Vec<String>,
label: Option<String>,
until: Option<String>,
expected_version: Option<u64>,
) -> CmdResult {
let clearing = profile_id.is_none();
let unlabelled = !clearing && label.is_none();
let result = client
.persona_binding_set(
&context_id,
&persona_did,
profile_id.as_deref(),
public_entries,
label.as_deref(),
until.as_deref(),
expected_version,
)
.await?;
if !is_json_output() {
if clearing {
println!("{DIM}Taken off — {persona_did} now shows nothing in {context_id}.{RESET}");
} else {
println!(
"{DIM}A context only ever gets a copy: the face's values were written into \
{context_id}. Editing an attribute refreshes it; copies go down, and nothing reads \
up.{RESET}"
);
if unlabelled {
println!(
"{DIM}No --label: {context_id} is given no name for this face. Your own name \
for it stays yours.{RESET}"
);
}
}
}
print_result("Binding:", &result)
}
pub async fn cmd_binding_get(
client: &VtaClient,
context_id: String,
persona_did: String,
) -> CmdResult {
let result = client
.persona_binding_get(&context_id, &persona_did)
.await?;
print_result("Binding:", &result)
}
pub async fn cmd_binding_list(
client: &VtaClient,
context_id: String,
limit: Option<std::num::NonZeroU64>,
cursor: Option<String>,
) -> CmdResult {
let result = client
.persona_binding_list(&context_id, limit, cursor.as_deref())
.await?;
print_result("Bindings:", &result)
}
pub async fn cmd_contact_put(
client: &VtaClient,
context_id: String,
subject_did: String,
known_by_persona: String,
document: ContactDocument,
credential_refs: Vec<String>,
notes: Option<String>,
) -> CmdResult {
let result = client
.persona_contact_put(
&context_id,
&subject_did,
&known_by_persona,
document,
credential_refs,
notes.as_deref(),
)
.await?;
print_result("Contact:", &result)
}
pub async fn cmd_contact_get(
client: &VtaClient,
context_id: String,
contact_id: String,
rev: Option<std::num::NonZeroU64>,
include_history: bool,
) -> CmdResult {
let result = client
.persona_contact_get(&context_id, &contact_id, rev, include_history)
.await?;
print_result("Contact:", &result)
}
pub async fn cmd_contact_list(
client: &VtaClient,
context_id: String,
known_by_persona: Option<String>,
changed_since: Option<String>,
limit: Option<std::num::NonZeroU64>,
cursor: Option<String>,
) -> CmdResult {
let result = client
.persona_contact_list(
&context_id,
known_by_persona.as_deref(),
changed_since.as_deref(),
limit,
cursor.as_deref(),
)
.await?;
print_result("Contacts:", &result)
}
pub async fn cmd_contact_delete(
client: &VtaClient,
context_id: String,
contact_id: String,
) -> CmdResult {
let result = client
.persona_contact_delete(&context_id, &contact_id)
.await?;
print_result("Result:", &result)
}
pub async fn cmd_disclosure_preview(
client: &VtaClient,
context_id: String,
persona_did: String,
verifier_did: String,
requested_claims: Vec<String>,
purpose: Option<String>,
renderer: Option<String>,
) -> CmdResult {
let result = client
.persona_disclosure_preview(
&context_id,
&persona_did,
&verifier_did,
requested_claims,
purpose.as_deref(),
renderer.as_deref(),
)
.await?;
if is_json_output() {
print_json(&result)?;
return Ok(());
}
render_preview(&result, &context_id, &verifier_did, purpose.as_deref());
Ok(())
}
fn render_preview(result: &Value, context_id: &str, verifier_did: &str, purpose: Option<&str>) {
let (Some(preview_id), Some(claims)) = (
result.get("previewId").and_then(Value::as_str),
result.get("claims").and_then(Value::as_array),
) else {
println!(
"{YELLOW}Preview response was not in the expected shape; showing it as received.{RESET}"
);
println!(
"{}",
serde_json::to_string_pretty(result).unwrap_or_else(|_| format!("{result:?}"))
);
return;
};
println!();
println!("{BOLD}Before anything leaves{RESET} — nothing has been sent.");
println!();
println!(" {DIM}To{RESET} {verifier_did}");
println!(" {DIM}Context{RESET} {context_id}");
if let Some(p) = purpose {
println!(" {DIM}Purpose{RESET} {p}");
}
if let Some(subject) = result.get("subject").and_then(Value::as_str) {
println!(" {DIM}As{RESET} {subject}");
}
if let Some(r) = result.get("renderer") {
let id = r.get("id").and_then(Value::as_str).unwrap_or("?");
let drops: Vec<&str> = r
.get("drops")
.and_then(Value::as_array)
.map(|a| a.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
if drops.is_empty() {
println!(" {DIM}Format{RESET} {id} {DIM}(carries everything){RESET}");
} else {
println!(
" {DIM}Format{RESET} {id} {YELLOW}— discards: {}{RESET}",
drops.join(", ")
);
}
}
if let Some(c) = result.get("correlation") {
let severity = c.get("severity").and_then(Value::as_str).unwrap_or("none");
let colour = match severity {
"high" => RED,
"low" => YELLOW,
_ => DIM,
};
let reason = c.get("reason").and_then(Value::as_str).unwrap_or("");
println!(
" {DIM}Linked{RESET} {colour}{}{RESET} {reason}",
severity.to_uppercase()
);
}
let anomalous: Vec<&str> = result
.get("anomalous")
.and_then(Value::as_array)
.map(|a| a.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
println!();
if claims.is_empty() {
println!(" {DIM}No attributes would leave.{RESET}");
}
for claim in claims {
let ty = claim.get("type").and_then(Value::as_str).unwrap_or("?");
let provenance = claim
.get("provenance")
.and_then(Value::as_str)
.unwrap_or("");
let rung = claim.get("rung").and_then(Value::as_str).unwrap_or("");
let stale = claim.get("stale").and_then(Value::as_bool).unwrap_or(false);
let fresh = claim
.get("newToThisVerifier")
.and_then(Value::as_bool)
.unwrap_or(false);
let odd = anomalous.contains(&ty);
let shown = if let Some(p) = claim.get("predicate") {
let op = p.get("op").and_then(Value::as_str).unwrap_or("?");
let arg = p.get("arg").map(render_scalar).unwrap_or_default();
format!("{DIM}proves {op} {arg} — the value itself is not sent{RESET}")
} else if stale {
format!("{YELLOW}stale — will NOT be sent{RESET}")
} else {
claim.get("value").map(render_scalar).unwrap_or_default()
};
let mark = if odd || fresh {
format!("{RED}!{RESET}")
} else {
" ".into()
};
println!(" {mark} {CYAN}{ty}{RESET}");
println!(" {shown}");
let mut notes: Vec<String> = Vec::new();
if !provenance.is_empty() {
notes.push(provenance.to_string());
}
if !rung.is_empty() {
notes.push(rung.to_string());
}
if fresh {
notes.push("new to this verifier".into());
}
if odd {
notes.push("unusual for the stated purpose".into());
}
if !notes.is_empty() {
println!(" {DIM}{}{RESET}", notes.join(" · "));
}
}
println!();
if let Some(expiry) = result.get("expiresAt").and_then(Value::as_str) {
println!(" {DIM}This preview expires at {expiry} and can be used once.{RESET}");
}
println!(
" Disclose with: {BOLD}persona disclosure present --context {context_id} \
--preview-id {preview_id}{RESET}"
);
println!();
}
fn render_scalar(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
pub async fn cmd_disclosure_present(
client: &VtaClient,
context_id: String,
preview_id: String,
challenge: Option<String>,
mint: Option<Value>,
) -> CmdResult {
let result = client
.persona_disclosure_present(&context_id, &preview_id, challenge.as_deref(), mint)
.await?;
if !is_json_output()
&& result
.get("document")
.and_then(|d| d.get("unsigned"))
.and_then(Value::as_bool)
.unwrap_or(false)
{
println!(
"{YELLOW}This document is UNSIGNED. Sign it with the persona's key \
(`keys derive-and-sign-document`) before sending it to a verifier.{RESET}"
);
}
print_result("What left:", &result)
}
pub async fn cmd_disclosure_history(
client: &VtaClient,
context_id: Option<String>,
verifier_did: Option<String>,
attribute_type: Option<String>,
since: Option<String>,
limit: Option<std::num::NonZeroU64>,
cursor: Option<String>,
) -> CmdResult {
let result = client
.persona_disclosure_history(
context_id.as_deref(),
verifier_did.as_deref(),
attribute_type.as_deref(),
since.as_deref(),
limit,
cursor.as_deref(),
)
.await?;
print_result("What has left:", &result)
}
pub async fn cmd_correlate(
client: &VtaClient,
attribute_id: Option<String>,
profile_id: Option<String>,
candidate: Option<Value>,
) -> CmdResult {
let result = client
.persona_correlation_analyze(attribute_id.as_deref(), profile_id.as_deref(), candidate)
.await?;
print_result("Links:", &result)
}
pub async fn cmd_renderers(client: &VtaClient) -> CmdResult {
let result = client.persona_renderers_list().await?;
print_result("Renderers:", &result)
}
pub async fn cmd_local_profile_put(
client: &VtaClient,
context_id: String,
name: String,
entries: Vec<LocalProfileEntry>,
profile_id: Option<String>,
expected_version: Option<u64>,
) -> CmdResult {
let result = client
.persona_local_profile_put(
&context_id,
&name,
entries,
profile_id.as_deref(),
expected_version,
)
.await?;
print_result("Local profile:", &result)
}
pub async fn cmd_local_profile_get(
client: &VtaClient,
context_id: String,
profile_id: String,
) -> CmdResult {
let result = client
.persona_local_profile_get(&context_id, &profile_id)
.await?;
print_result("Local profile:", &result)
}
pub async fn cmd_local_profile_list(
client: &VtaClient,
context_id: String,
limit: Option<std::num::NonZeroU64>,
cursor: Option<String>,
) -> CmdResult {
let result = client
.persona_local_profile_list(&context_id, limit, cursor.as_deref())
.await?;
print_result("Local profiles:", &result)
}
pub async fn cmd_local_profile_delete(
client: &VtaClient,
context_id: String,
profile_id: String,
unbind: bool,
) -> CmdResult {
let result = client
.persona_local_profile_delete(&context_id, &profile_id, unbind)
.await?;
print_result("Result:", &result)
}
pub async fn cmd_local_binding_set(
client: &VtaClient,
context_id: String,
persona_did: String,
profile_id: Option<String>,
label: Option<String>,
until: Option<String>,
expected_version: Option<u64>,
) -> CmdResult {
let result = client
.persona_local_binding_set(
&context_id,
&persona_did,
profile_id.as_deref(),
label.as_deref(),
until.as_deref(),
expected_version,
)
.await?;
print_result("Local binding:", &result)
}