1use std::io::{IsTerminal, Read};
15
16use serde::de::DeserializeOwned;
17use serde_json::{Value, json};
18use vta_sdk::prelude::*;
19use vti_specs::external as ext;
20
21use crate::render::print_json;
22
23type CliResult = Result<(), Box<dyn std::error::Error>>;
24
25fn payload<P: DeserializeOwned>(task: &str, v: Value) -> Result<P, Box<dyn std::error::Error>> {
27 serde_json::from_value(v).map_err(|e| format!("{task}: the request is not valid: {e}").into())
28}
29
30fn settings_arg(raw: &str) -> Result<Value, Box<dyn std::error::Error>> {
32 let text = match raw.strip_prefix('@') {
33 Some(path) => std::fs::read_to_string(path).map_err(|e| format!("read {path}: {e}"))?,
34 None => raw.to_string(),
35 };
36 serde_json::from_str(&text).map_err(|e| format!("--settings is not JSON: {e}").into())
37}
38
39fn show<T: serde::Serialize>(v: &T) -> CliResult {
40 print_json(v)?;
41 Ok(())
42}
43
44pub async fn cmd_list(
46 client: &VtaClient,
47 context: &str,
48 state: Option<&str>,
49 model: Option<&str>,
50) -> CliResult {
51 let mut v = json!({ "context": context });
52 if let Some(s) = state {
53 v["state"] = json!(s);
54 }
55 if let Some(m) = model {
56 v["model"] = json!(m);
57 }
58 let req = payload::<ext::accounts::list::v0_2::Payload>("external/accounts/list", v)?;
59 show(&client.external_accounts_list(&req).await?)
60}
61
62pub async fn cmd_get(client: &VtaClient, context: &str, id: &str) -> CliResult {
64 let req = payload::<ext::accounts::get::v0_2::Payload>(
65 "external/accounts/get",
66 json!({ "context": context, "id": id }),
67 )?;
68 show(&client.external_accounts_get(&req).await?)
69}
70
71pub async fn cmd_create(
73 client: &VtaClient,
74 context: &str,
75 id: &str,
76 label: &str,
77 settings: &str,
78) -> CliResult {
79 let req = payload::<ext::accounts::create::v0_2::Payload>(
80 "external/accounts/create",
81 json!({ "context": context, "id": id, "label": label, "settings": settings_arg(settings)? }),
82 )?;
83 show(&client.external_accounts_create(&req).await?)
84}
85
86pub async fn cmd_update(
88 client: &VtaClient,
89 context: &str,
90 id: &str,
91 label: Option<&str>,
92 settings: Option<&str>,
93) -> CliResult {
94 let mut v = json!({ "context": context, "id": id });
95 if let Some(l) = label {
96 v["label"] = json!(l);
97 }
98 if let Some(s) = settings {
99 v["settings"] = settings_arg(s)?;
100 }
101 let req = payload::<ext::accounts::update::v0_2::Payload>("external/accounts/update", v)?;
102 show(&client.external_accounts_update(&req).await?)
103}
104
105pub async fn cmd_secret_set(client: &VtaClient, context: &str, id: &str) -> CliResult {
107 let account = client
110 .external_accounts_get(&payload(
111 "external/accounts/get",
112 json!({ "context": context, "id": id }),
113 )?)
114 .await?;
115 let access_key_id = serde_json::to_value(&account.account.settings)?
116 .get("accessKeyId")
117 .and_then(Value::as_str)
118 .map(str::to_string);
119 let secret = zeroize::Zeroizing::new(if std::io::stdin().is_terminal() {
120 dialoguer::Password::new()
121 .with_prompt(format!("Secret for {context}/{id}"))
122 .interact()?
123 } else {
124 let mut s = String::new();
125 std::io::stdin().read_to_string(&mut s)?;
126 s.trim_end_matches(['\r', '\n']).to_string()
127 });
128 if secret.is_empty() {
129 return Err("no secret was given".into());
130 }
131 let wrapping = client.get_wrapping_key().await?;
132 let armored = vta_sdk::client::seal_external_secret(
133 &wrapping.wrapping_key,
134 context,
135 id,
136 &secret,
137 access_key_id.as_deref(),
138 )
139 .await?;
140 drop(secret);
141 let req = payload::<ext::accounts::secret::set::v0_1::Payload>(
142 "external/accounts/secret/set",
143 json!({ "context": context, "id": id, "wrappingKeyId": wrapping.key_id, "sealedSecret": armored }),
144 )?;
145 show(&client.external_accounts_secret_set(&req).await?)
146}
147
148#[allow(clippy::too_many_arguments)]
150pub async fn cmd_bind(
151 client: &VtaClient,
152 context: &str,
153 id: &str,
154 consumer: &str,
155 prefixes: &[String],
156 actions: &[String],
157 max_ttl_seconds: u32,
158 rate_per_minute: u32,
159) -> CliResult {
160 let mut binding = json!({
161 "consumer": consumer,
162 "maxTtlSeconds": max_ttl_seconds,
163 "ratePerMinute": rate_per_minute,
164 });
165 if !prefixes.is_empty() || !actions.is_empty() {
166 binding["scopeCeiling"] = json!({ "prefixes": prefixes, "actions": actions });
167 }
168 let req = payload::<ext::accounts::bindings::grant::v0_2::Payload>(
169 "external/accounts/bindings/grant",
170 json!({ "context": context, "id": id, "binding": binding }),
171 )?;
172 show(&client.external_accounts_bindings_grant(&req).await?)
173}
174
175pub async fn cmd_unbind(client: &VtaClient, context: &str, id: &str, consumer: &str) -> CliResult {
177 let req = payload::<ext::accounts::bindings::revoke::v0_2::Payload>(
178 "external/accounts/bindings/revoke",
179 json!({ "context": context, "id": id, "consumer": consumer }),
180 )?;
181 show(&client.external_accounts_bindings_revoke(&req).await?)
182}
183
184pub async fn cmd_setup(client: &VtaClient, context: &str, id: &str) -> CliResult {
186 let req = payload::<ext::accounts::setup::v0_1::Payload>(
187 "external/accounts/setup",
188 json!({ "context": context, "id": id }),
189 )?;
190 show(&client.external_accounts_setup(&req).await?)
191}
192
193pub async fn cmd_probe(client: &VtaClient, context: &str, id: &str) -> CliResult {
195 let req = payload::<ext::accounts::probe::v0_2::Payload>(
196 "external/accounts/probe",
197 json!({ "context": context, "id": id }),
198 )?;
199 show(&client.external_accounts_probe(&req).await?)
200}
201
202#[derive(Debug, Clone, Copy)]
204pub enum Lifecycle {
205 Suspend,
206 Resume,
207 Archive,
208 Restore,
209 Delete,
210}
211
212pub async fn cmd_lifecycle(
214 client: &VtaClient,
215 verb: Lifecycle,
216 context: &str,
217 id: &str,
218 reason: Option<&str>,
219) -> CliResult {
220 let mut v = json!({ "context": context, "id": id });
221 if let Some(r) = reason {
222 v["reason"] = json!(r);
223 }
224 match verb {
225 Lifecycle::Suspend => show(
226 &client
227 .external_accounts_suspend(&payload("external/accounts/suspend", v)?)
228 .await?,
229 ),
230 Lifecycle::Resume => show(
231 &client
232 .external_accounts_resume(&payload("external/accounts/resume", v)?)
233 .await?,
234 ),
235 Lifecycle::Archive => show(
236 &client
237 .external_accounts_archive(&payload("external/accounts/archive", v)?)
238 .await?,
239 ),
240 Lifecycle::Restore => show(
241 &client
242 .external_accounts_restore(&payload("external/accounts/restore", v)?)
243 .await?,
244 ),
245 Lifecycle::Delete => show(
246 &client
247 .external_accounts_delete(&payload("external/accounts/delete", v)?)
248 .await?,
249 ),
250 }
251}
252
253pub async fn cmd_issue(
259 client: &VtaClient,
260 context: &str,
261 account: &str,
262 prefix: Option<&str>,
263 actions: &[String],
264 ttl_seconds: u32,
265) -> CliResult {
266 let mut scope = json!({});
267 if let Some(p) = prefix {
268 scope["prefix"] = json!(p);
269 }
270 if !actions.is_empty() {
271 scope["actions"] = json!(actions);
272 }
273 let req = payload::<ext::credentials::issue::v0_3::Payload>(
274 "external/credentials/issue",
275 json!({ "context": context, "account": account, "scope": scope, "ttlSeconds": ttl_seconds }),
276 )?;
277 show(&client.external_credentials_issue(&req).await?)
278}
279
280pub async fn cmd_issue_presign(
289 client: &VtaClient,
290 context: &str,
291 account: &str,
292 batch: &str,
293 ttl_seconds: u32,
294) -> CliResult {
295 let raw = match batch.strip_prefix('@') {
296 Some(path) => std::fs::read_to_string(path)
297 .map_err(|e| format!("cannot read the presign batch {path}: {e}"))?,
298 None => batch.to_string(),
299 };
300 let presign: serde_json::Value =
301 serde_json::from_str(&raw).map_err(|e| format!("the presign batch is not JSON: {e}"))?;
302 let req = payload::<ext::credentials::issue::v0_3::Payload>(
303 "external/credentials/issue",
304 json!({ "context": context, "account": account, "presign": presign, "ttlSeconds": ttl_seconds }),
305 )?;
306 let answers = client.external_credentials_issue_presign(&req).await?;
307 match answers.as_slice() {
308 [one] => show(one),
309 parts => {
310 eprintln!(
311 "the VTA could not seal this batch in one answer; it was issued in {} parts",
312 parts.len()
313 );
314 show(&parts)
315 }
316 }
317}