Skip to main content

vta_cli_common/commands/
vault.rs

1//! `vault …` operator/agent commands (online, via the trust-task dispatcher).
2//!
3//! Thin wrappers over the `VtaClient::vault_*` methods plus the seal/open
4//! helpers. Secret-bearing operations use `didcomm-authcrypt` sealed envelopes
5//! and therefore require the DIDComm transport (the seal is produced with the
6//! caller's own keys):
7//! - `upsert` seals the cleartext secret to the VTA before sending.
8//! - `release` opens the JWE the VTA seals back to the caller.
9//!
10//! Capability gates (server-side): list/get → `VaultRead`, upsert/delete →
11//! `VaultWrite`, release → `FillRelease`, proxy-login → `ProxyLogin`,
12//! sign-trust-task → `SignTrustTask`.
13
14use serde_json::{Value, json};
15use vta_sdk::client::VtaClient;
16
17use crate::render::{DIM, RESET, is_json_output, print_json};
18
19fn print_result(label: &str, value: &Value) -> Result<(), Box<dyn std::error::Error>> {
20    if is_json_output() {
21        print_json(value)?;
22    } else {
23        println!("{label}");
24        println!("{}", serde_json::to_string_pretty(value)?);
25    }
26    Ok(())
27}
28
29/// `vault list` — metadata only (no secrets). `filters` is the wire filter
30/// object (`None` → all entries). `status` selects the lifecycle view
31/// (`active` default / `archived` / `deleted` / `all`) and is merged into the
32/// filter object.
33pub async fn cmd_vault_list(
34    client: &VtaClient,
35    filters: Option<Value>,
36    status: Option<String>,
37) -> Result<(), Box<dyn std::error::Error>> {
38    let mut filters = filters.unwrap_or_else(|| json!({}));
39    if let Some(s) = status
40        && let Some(obj) = filters.as_object_mut()
41    {
42        obj.insert("status".to_string(), json!(s));
43    }
44    let result = client.vault_list(filters).await?;
45    print_result("Vault entries:", &result)
46}
47
48/// `vault get` — a single entry's metadata by id.
49pub async fn cmd_vault_get(
50    client: &VtaClient,
51    id: String,
52) -> Result<(), Box<dyn std::error::Error>> {
53    let result = client.vault_get(&id).await?;
54    print_result("Vault entry:", &result)
55}
56
57/// `vault delete` — soft-delete (recoverable) by default; `force` hard-deletes
58/// irreversibly. Optional optimistic-concurrency version check + audit reason.
59pub async fn cmd_vault_delete(
60    client: &VtaClient,
61    id: String,
62    expected_version: Option<u32>,
63    force: bool,
64    reason: Option<String>,
65) -> Result<(), Box<dyn std::error::Error>> {
66    let result = client
67        .vault_delete(&id, expected_version, force, reason.as_deref())
68        .await?;
69    if force {
70        println!("{DIM}Vault entry {id} permanently hard-deleted (no recovery).{RESET}");
71    } else {
72        let grace = result.get("graceUntil").and_then(Value::as_str);
73        match grace {
74            Some(g) => println!(
75                "{DIM}Vault entry {id} moved to trash — recoverable with `vault restore {id}` until {g}.{RESET}"
76            ),
77            None => println!(
78                "{DIM}Vault entry {id} soft-deleted — recoverable with `vault restore {id}`.{RESET}"
79            ),
80        }
81    }
82    print_result("Result:", &result)
83}
84
85/// `vault archive` — soft-disable an entry (restorable with `vault unarchive`).
86pub async fn cmd_vault_archive(
87    client: &VtaClient,
88    id: String,
89    expected_version: Option<u32>,
90    reason: Option<String>,
91) -> Result<(), Box<dyn std::error::Error>> {
92    let result = client
93        .vault_archive(&id, expected_version, reason.as_deref())
94        .await?;
95    println!("{DIM}Vault entry {id} archived — restore with `vault unarchive {id}`.{RESET}");
96    print_result("Result:", &result)
97}
98
99/// `vault unarchive` — return an archived entry to active.
100pub async fn cmd_vault_unarchive(
101    client: &VtaClient,
102    id: String,
103    expected_version: Option<u32>,
104    reason: Option<String>,
105) -> Result<(), Box<dyn std::error::Error>> {
106    let result = client
107        .vault_unarchive(&id, expected_version, reason.as_deref())
108        .await?;
109    println!("{DIM}Vault entry {id} unarchived.{RESET}");
110    print_result("Result:", &result)
111}
112
113/// `vault restore` — undelete a soft-deleted entry (only within the grace
114/// window).
115pub async fn cmd_vault_restore(
116    client: &VtaClient,
117    id: String,
118    expected_version: Option<u32>,
119    reason: Option<String>,
120) -> Result<(), Box<dyn std::error::Error>> {
121    let result = client
122        .vault_restore(&id, expected_version, reason.as_deref())
123        .await?;
124    println!("{DIM}Vault entry {id} restored to active.{RESET}");
125    print_result("Result:", &result)
126}
127
128/// `vault purge` — irreversibly hard-delete an entry, skipping any grace
129/// window.
130pub async fn cmd_vault_purge(
131    client: &VtaClient,
132    id: String,
133    expected_version: Option<u32>,
134    reason: Option<String>,
135) -> Result<(), Box<dyn std::error::Error>> {
136    let result = client
137        .vault_purge(&id, expected_version, reason.as_deref())
138        .await?;
139    println!("{DIM}Vault entry {id} permanently purged (no recovery).{RESET}");
140    print_result("Result:", &result)
141}
142
143/// `vault upsert` — create/update an entry. `entry` is the entry-fields payload
144/// (`contextId`, `targets`, `label`, `secretKind`, …); `secret`, when present,
145/// is the cleartext `VaultSecret` JSON, sealed here to the VTA before sending.
146///
147/// Sealing requires the DIDComm transport — a clear error is returned on REST.
148pub async fn cmd_vault_upsert(
149    client: &VtaClient,
150    entry: Value,
151    secret: Option<Value>,
152) -> Result<(), Box<dyn std::error::Error>> {
153    let sealed_secret = match secret {
154        Some(s) => {
155            let jwe = client.seal_vault_secret(s).await?;
156            Some(json!({ "envelope": "didcomm-authcrypt", "jwe": jwe }))
157        }
158        None => None,
159    };
160    let result = client.vault_upsert(entry, sealed_secret).await?;
161    print_result("Upserted entry:", &result)
162}
163
164/// `vault release` — release a secret sealed to the caller. Fetches the sealed
165/// envelope, opens it locally, and prints the cleartext `VaultSecret`. `target`
166/// is the optional site target the release is scoped to.
167pub async fn cmd_vault_release(
168    client: &VtaClient,
169    id: String,
170    target: Option<Value>,
171) -> Result<(), Box<dyn std::error::Error>> {
172    let mut payload = json!({ "id": id });
173    if let Some(t) = target {
174        payload["target"] = t;
175    }
176    let response = client.vault_release(payload).await?;
177
178    // The released secret rides in a `didcomm-authcrypt` envelope; open it with
179    // the caller's keys. Walk the documented shape
180    // (`sealedSecret.jwe`) and fall back to printing the raw response if the
181    // VTA emitted an envelope variant this client can't open.
182    let jwe = response
183        .get("sealedSecret")
184        .and_then(|s| s.get("jwe"))
185        .and_then(|j| j.as_str());
186    match jwe {
187        Some(jwe) => {
188            let secret = client.open_sealed_secret(jwe).await?;
189            print_result("Released secret (cleartext):", &secret)
190        }
191        None => {
192            eprintln!("{DIM}(no didcomm-authcrypt sealedSecret in response — printing raw){RESET}");
193            print_result("Release response:", &response)
194        }
195    }
196}
197
198/// `vault proxy-login` — mint a session as the entry's principal. `payload` is
199/// the full wire request (entry id + login parameters).
200pub async fn cmd_vault_proxy_login(
201    client: &VtaClient,
202    payload: Value,
203) -> Result<(), Box<dyn std::error::Error>> {
204    let result = client.vault_proxy_login(payload).await?;
205    print_result("Proxy-login result:", &result)
206}
207
208/// `vault sign-trust-task` — sign a Trust Task envelope as the entry's
209/// principal DID. `payload` is the full wire request (entry id + envelope).
210pub async fn cmd_vault_sign_trust_task(
211    client: &VtaClient,
212    payload: Value,
213) -> Result<(), Box<dyn std::error::Error>> {
214    let result = client.vault_sign_trust_task(payload).await?;
215    print_result("Signed envelope:", &result)
216}