1use std::io::{self, Write};
2
3use ratatui::{
4 layout::Constraint,
5 style::{Color, Modifier, Style},
6 widgets::{Block, Cell, Row, Table},
7};
8use vta_sdk::client::{CreateDidWebvhRequest, UpdateContextRequest};
9use vta_sdk::context_provision::{ContextProvisionBundle, ProvisionedDid};
10use vta_sdk::prelude::*;
11
12use crate::render::print_widget;
13
14pub struct ProvisionDidOptions {
15 pub server_id: Option<String>,
16 pub did_url: Option<String>,
17 pub portable: bool,
18 pub add_mediator_service: bool,
19 pub pre_rotation_count: u32,
20}
21
22pub async fn cmd_context_bootstrap(
23 client: &VtaClient,
24 id: &str,
25 name: &str,
26 description: Option<String>,
27 admin_label: Option<String>,
28) -> Result<(), Box<dyn std::error::Error>> {
29 let mut ctx_req = CreateContextRequest::new(id, name);
30 if let Some(desc) = description {
31 ctx_req = ctx_req.description(desc);
32 }
33 let ctx = client.create_context(ctx_req).await?;
34 println!("Context created:");
35 println!(" ID: {}", ctx.id);
36 println!(" Name: {}", ctx.name);
37 println!(" Base Path: {}", ctx.base_path);
38
39 let mut cred_req = GenerateCredentialsRequest::new("admin").contexts(vec![id.to_string()]);
40 if let Some(l) = admin_label {
41 cred_req = cred_req.label(l);
42 }
43 let resp = client.generate_credentials(cred_req).await?;
44 println!();
45 println!("Admin credential created:");
46 println!(" DID: {}", resp.did);
47 println!(" Role: admin");
48 println!();
49 println!("Credential (one-time secret — save this now):");
50 println!("{}", resp.credential);
51
52 Ok(())
53}
54
55pub async fn cmd_context_list(client: &VtaClient) -> Result<(), Box<dyn std::error::Error>> {
56 let resp = client.list_contexts().await?;
57
58 if resp.contexts.is_empty() {
59 println!("No contexts found.");
60 return Ok(());
61 }
62
63 let header_style = Style::default()
64 .fg(Color::White)
65 .add_modifier(Modifier::BOLD);
66 let header = Row::new(vec!["ID", "Name", "DID", "Base Path", "Created"])
67 .style(header_style)
68 .bottom_margin(1);
69
70 let rows: Vec<Row> = resp
71 .contexts
72 .iter()
73 .map(|ctx| {
74 let did = ctx.did.clone().unwrap_or_else(|| "\u{2014}".into());
75 let created = ctx.created_at.format("%Y-%m-%d").to_string();
76
77 Row::new(vec![
78 Cell::from(ctx.id.clone()),
79 Cell::from(ctx.name.clone()),
80 Cell::from(did).style(Style::default().fg(Color::DarkGray)),
81 Cell::from(ctx.base_path.clone()),
82 Cell::from(created),
83 ])
84 })
85 .collect();
86
87 let title = format!(" Contexts ({}) ", resp.contexts.len());
88
89 let table = Table::new(
90 rows,
91 [
92 Constraint::Length(16), Constraint::Min(20), Constraint::Length(30), Constraint::Length(16), Constraint::Length(10), ],
98 )
99 .header(header)
100 .column_spacing(2)
101 .block(
102 Block::bordered()
103 .title(title)
104 .border_style(Style::default().fg(Color::DarkGray)),
105 );
106
107 let height = resp.contexts.len() as u16 + 4;
108 print_widget(table, height);
109
110 Ok(())
111}
112
113pub async fn cmd_context_get(
114 client: &VtaClient,
115 id: &str,
116) -> Result<(), Box<dyn std::error::Error>> {
117 let resp = client.get_context(id).await?;
118 println!("ID: {}", resp.id);
119 println!("Name: {}", resp.name);
120 println!(
121 "DID: {}",
122 resp.did.as_deref().unwrap_or("(not set)")
123 );
124 println!(
125 "Description: {}",
126 resp.description.as_deref().unwrap_or("(not set)")
127 );
128 println!("Base Path: {}", resp.base_path);
129 println!("Created At: {}", resp.created_at);
130 println!("Updated At: {}", resp.updated_at);
131 Ok(())
132}
133
134pub async fn cmd_context_create(
135 client: &VtaClient,
136 id: &str,
137 name: &str,
138 description: Option<String>,
139) -> Result<(), Box<dyn std::error::Error>> {
140 let req = CreateContextRequest {
141 id: id.to_string(),
142 name: name.to_string(),
143 description,
144 };
145 let resp = client.create_context(req).await?;
146 println!("Context created:");
147 println!(" ID: {}", resp.id);
148 println!(" Name: {}", resp.name);
149 println!(" Base Path: {}", resp.base_path);
150 Ok(())
151}
152
153pub async fn cmd_context_update(
154 client: &VtaClient,
155 id: &str,
156 name: Option<String>,
157 did: Option<String>,
158 description: Option<String>,
159) -> Result<(), Box<dyn std::error::Error>> {
160 let req = UpdateContextRequest {
161 name,
162 did,
163 description,
164 };
165 let resp = client.update_context(id, req).await?;
166 println!("Context updated:");
167 println!(" ID: {}", resp.id);
168 println!(" Name: {}", resp.name);
169 println!(
170 " DID: {}",
171 resp.did.as_deref().unwrap_or("(not set)")
172 );
173 println!(
174 " Description: {}",
175 resp.description.as_deref().unwrap_or("(not set)")
176 );
177 println!(" Updated At: {}", resp.updated_at);
178 Ok(())
179}
180
181pub async fn cmd_context_update_did(
182 client: &VtaClient,
183 id: &str,
184 did: &str,
185) -> Result<(), Box<dyn std::error::Error>> {
186 let resp = client.update_context_did(id, did).await?;
187 println!("Context DID updated:");
188 println!(" ID: {}", resp.id);
189 println!(
190 " DID: {}",
191 resp.did.as_deref().unwrap_or("(not set)")
192 );
193 println!(" Updated At: {}", resp.updated_at);
194 Ok(())
195}
196
197pub async fn cmd_context_delete(
198 client: &VtaClient,
199 id: &str,
200 force: bool,
201) -> Result<(), Box<dyn std::error::Error>> {
202 let preview = client.preview_delete_context(id).await?;
204
205 let has_resources = !preview.keys.is_empty()
206 || !preview.webvh_dids.is_empty()
207 || !preview.acl_entries_removed.is_empty()
208 || !preview.acl_entries_updated.is_empty();
209
210 if has_resources {
211 println!(
212 "Deleting context '{}' will remove the following resources:\n",
213 id
214 );
215
216 if !preview.keys.is_empty() {
217 println!(" Keys ({}):", preview.keys.len());
218 for key in &preview.keys {
219 println!(" - {key}");
220 }
221 }
222
223 if !preview.webvh_dids.is_empty() {
224 println!(" WebVH DIDs ({}):", preview.webvh_dids.len());
225 for did in &preview.webvh_dids {
226 println!(" - {did}");
227 }
228 }
229
230 if !preview.acl_entries_removed.is_empty() {
231 println!(
232 " ACL entries removed ({}):",
233 preview.acl_entries_removed.len()
234 );
235 for did in &preview.acl_entries_removed {
236 println!(" - {did}");
237 }
238 }
239
240 if !preview.acl_entries_updated.is_empty() {
241 println!(
242 " ACL entries updated (context removed from access list) ({}):",
243 preview.acl_entries_updated.len()
244 );
245 for did in &preview.acl_entries_updated {
246 println!(" - {did}");
247 }
248 }
249
250 println!();
251
252 if !force {
253 print!("Proceed with deletion? [y/N] ");
254 io::stdout().flush()?;
255
256 let mut input = String::new();
257 io::stdin().read_line(&mut input)?;
258 let input = input.trim().to_lowercase();
259
260 if input != "y" && input != "yes" {
261 println!("Aborted.");
262 return Ok(());
263 }
264 }
265 }
266
267 client.delete_context(id, true).await?;
268 println!("Context deleted: {id}");
269 Ok(())
270}
271
272pub async fn cmd_context_provision(
273 client: &VtaClient,
274 id: &str,
275 name: &str,
276 description: Option<String>,
277 admin_label: Option<String>,
278 did_opts: Option<ProvisionDidOptions>,
279) -> Result<(), Box<dyn std::error::Error>> {
280 eprintln!("Creating context '{id}'...");
282 let mut ctx_req = CreateContextRequest::new(id, name);
283 if let Some(desc) = description {
284 ctx_req = ctx_req.description(desc);
285 }
286 client.create_context(ctx_req).await?;
287
288 eprintln!("Generating admin credentials...");
290 let mut cred_req = GenerateCredentialsRequest::new("admin").contexts(vec![id.to_string()]);
291 if let Some(l) = admin_label {
292 cred_req = cred_req.label(l);
293 }
294 let cred_resp = client.generate_credentials(cred_req).await?;
295
296 let config = client.get_config().await?;
298
299 let provisioned_did = if let Some(opts) = did_opts {
301 eprintln!("Creating WebVH DID...");
302 let req = CreateDidWebvhRequest {
303 context_id: id.to_string(),
304 server_id: opts.server_id,
305 url: opts.did_url,
306 path: None,
307 label: Some(id.to_string()),
308 portable: opts.portable,
309 add_mediator_service: opts.add_mediator_service,
310 additional_services: None,
311 pre_rotation_count: opts.pre_rotation_count,
312 };
313 let did_result = client.create_did_webvh(req).await?;
314
315 eprintln!("Fetching DID key secrets...");
317 let mut secrets: Vec<SecretEntry> = Vec::new();
318 secrets.push(
320 client
321 .get_key_secret(&did_result.signing_key_id)
322 .await?
323 .into(),
324 );
325 secrets.push(client.get_key_secret(&did_result.ka_key_id).await?.into());
327 for i in 0..did_result.pre_rotation_key_count {
329 let pre_rot_id = format!("{}#pre-rotation-{i}", did_result.did);
330 secrets.push(client.get_key_secret(&pre_rot_id).await?.into());
331 }
332
333 Some(ProvisionedDid {
334 id: did_result.did,
335 did_document: did_result.did_document,
336 log_entry: did_result.log_entry,
337 secrets,
338 })
339 } else {
340 None
341 };
342
343 let bundle = ContextProvisionBundle {
345 context_id: id.to_string(),
346 context_name: name.to_string(),
347 vta_url: config.public_url,
348 vta_did: config.community_vta_did,
349 credential: cred_resp.credential,
350 admin_did: cred_resp.did,
351 did: provisioned_did,
352 };
353
354 let encoded = bundle.encode().map_err(|e| format!("{e}"))?;
355
356 eprintln!();
358 eprintln!("\x1b[1;33m╔══════════════════════════════════════════════════════════════╗");
359 eprintln!("║ Context provision bundle (contains secrets — save securely) ║");
360 eprintln!("╚══════════════════════════════════════════════════════════════╝\x1b[0m");
361 eprintln!();
362 eprintln!(" Context: {} ({})", id, name);
363 eprintln!(" Admin DID: {}", bundle.admin_did);
364 if let Some(ref did) = bundle.did {
365 eprintln!(" DID: {}", did.id);
366 if did.log_entry.is_some() {
367 eprintln!(" (includes log entry for self-hosting)");
368 }
369 }
370 eprintln!();
371 println!("{encoded}");
372 eprintln!();
373
374 Ok(())
375}
376
377async fn credential_from_key(
379 client: &VtaClient,
380 key_id: &str,
381 vta_did: &str,
382 vta_url: Option<&str>,
383) -> Result<(String, String), Box<dyn std::error::Error>> {
384 let secret = client.get_key_secret(key_id).await?;
385 let seed = decode_private_key_multibase(&secret.private_key_multibase)
386 .map_err(|e| format!("Cannot decode key secret: {e}"))?;
387 let public_key = ed25519_dalek::SigningKey::from_bytes(&seed)
388 .verifying_key()
389 .to_bytes();
390 let did = format!("did:key:{}", ed25519_multibase_pubkey(&public_key));
391
392 let bundle = CredentialBundle {
393 did: did.clone(),
394 private_key_multibase: secret.private_key_multibase,
395 vta_did: vta_did.to_string(),
396 vta_url: vta_url.map(String::from),
397 };
398 let encoded = bundle.encode().map_err(|e| format!("{e}"))?;
399 Ok((encoded, did))
400}
401
402pub async fn cmd_context_reprovision(
403 client: &VtaClient,
404 id: &str,
405 key_id: Option<String>,
406 admin_label: Option<String>,
407) -> Result<(), Box<dyn std::error::Error>> {
408 eprintln!("Fetching context '{id}'...");
410 let ctx = client.get_context(id).await?;
411
412 let config = client.get_config().await?;
414 let vta_did = config
415 .community_vta_did
416 .as_deref()
417 .ok_or("VTA DID not configured")?;
418
419 let (admin_credential, admin_did) = if let Some(ref kid) = key_id {
421 eprintln!("Using key '{kid}'...");
423 credential_from_key(client, kid, vta_did, config.public_url.as_deref()).await?
424 } else {
425 let keys_resp = client.list_keys(0, 10000, Some("active"), Some(id)).await?;
427 let ed25519_keys: Vec<_> = keys_resp
428 .keys
429 .iter()
430 .filter(|k| k.key_type == KeyType::Ed25519)
431 .collect();
432
433 eprintln!();
434 eprintln!("Select an admin credential key for context '{id}':");
435 eprintln!();
436 for (i, key) in ed25519_keys.iter().enumerate() {
437 let label = key
438 .label
439 .as_deref()
440 .map(|l| format!(" ({l})"))
441 .unwrap_or_default();
442 eprintln!(" [{}] {}{}", i + 1, key.key_id, label);
443 }
444 let new_option = ed25519_keys.len() + 1;
445 eprintln!(" [{}] Create a new admin key", new_option);
446 eprintln!();
447 eprint!("Choice [{}]: ", new_option);
448 io::stderr().flush()?;
449
450 let mut input = String::new();
451 io::stdin().read_line(&mut input)?;
452 let input = input.trim();
453
454 let choice: usize = if input.is_empty() {
456 new_option
457 } else {
458 input
459 .parse()
460 .map_err(|_| format!("Invalid choice: {input}"))?
461 };
462
463 if choice == new_option {
464 eprintln!("Creating new admin key...");
466 let key_resp = client
467 .create_key(CreateKeyRequest {
468 key_type: KeyType::Ed25519,
469 derivation_path: None,
470 key_id: None,
471 mnemonic: None,
472 label: admin_label.or_else(|| Some("admin".to_string())),
473 context_id: Some(id.to_string()),
474 })
475 .await?;
476 credential_from_key(
477 client,
478 &key_resp.key_id,
479 vta_did,
480 config.public_url.as_deref(),
481 )
482 .await?
483 } else if choice >= 1 && choice <= ed25519_keys.len() {
484 let selected = &ed25519_keys[choice - 1];
485 eprintln!("Using key '{}'...", selected.key_id);
486 credential_from_key(
487 client,
488 &selected.key_id,
489 vta_did,
490 config.public_url.as_deref(),
491 )
492 .await?
493 } else {
494 return Err(format!("Invalid choice: {choice}").into());
495 }
496 };
497
498 if client.get_acl(&admin_did).await.is_err() {
500 eprintln!("Creating ACL entry for {admin_did}...");
501 client
502 .create_acl(
503 vta_sdk::client::CreateAclRequest::new(&admin_did, "admin")
504 .contexts(vec![id.to_string()]),
505 )
506 .await?;
507 }
508
509 let provisioned_did = if let Some(ref did_id) = ctx.did {
511 eprintln!("Fetching DID material...");
512
513 let log_resp = client.get_did_webvh_log(did_id).await?;
515 let (did_document, log_entry) = if let Some(ref log_str) = log_resp.log {
516 let parsed: serde_json::Value = serde_json::from_str(log_str)
517 .map_err(|e| format!("failed to parse DID log: {e}"))?;
518 let doc = parsed.get("state").cloned();
519 (doc, Some(log_str.clone()))
520 } else {
521 (None, None)
522 };
523
524 let secrets_bundle = client.fetch_did_secrets_bundle(id).await?;
526
527 Some(ProvisionedDid {
528 id: did_id.clone(),
529 did_document,
530 log_entry,
531 secrets: secrets_bundle.secrets,
532 })
533 } else {
534 None
535 };
536
537 let bundle = ContextProvisionBundle {
539 context_id: id.to_string(),
540 context_name: ctx.name.clone(),
541 vta_url: config.public_url,
542 vta_did: config.community_vta_did,
543 credential: admin_credential,
544 admin_did,
545 did: provisioned_did,
546 };
547
548 let encoded = bundle.encode().map_err(|e| format!("{e}"))?;
549
550 eprintln!();
552 eprintln!("\x1b[1;33m╔══════════════════════════════════════════════════════════════╗");
553 eprintln!("║ Context provision bundle (contains secrets — save securely) ║");
554 eprintln!("╚══════════════════════════════════════════════════════════════╝\x1b[0m");
555 eprintln!();
556 eprintln!(" Context: {} ({})", id, ctx.name);
557 eprintln!(" Admin DID: {}", bundle.admin_did);
558 if let Some(ref did) = bundle.did {
559 eprintln!(" DID: {}", did.id);
560 }
561 eprintln!();
562 println!("{encoded}");
563 eprintln!();
564
565 Ok(())
566}