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 did_document: None,
313 did_log: None,
314 set_primary: true,
315 signing_key_id: None,
316 ka_key_id: None,
317 };
318 let did_result = client.create_did_webvh(req).await?;
319
320 eprintln!("Fetching DID key secrets...");
322 let mut secrets: Vec<SecretEntry> = Vec::new();
323 secrets.push(
325 client
326 .get_key_secret(&did_result.signing_key_id)
327 .await?
328 .into(),
329 );
330 secrets.push(client.get_key_secret(&did_result.ka_key_id).await?.into());
332 for i in 0..did_result.pre_rotation_key_count {
334 let pre_rot_id = format!("{}#pre-rotation-{i}", did_result.did);
335 secrets.push(client.get_key_secret(&pre_rot_id).await?.into());
336 }
337
338 Some(ProvisionedDid {
339 id: did_result.did,
340 did_document: did_result.did_document,
341 log_entry: did_result.log_entry,
342 secrets,
343 })
344 } else {
345 None
346 };
347
348 let bundle = ContextProvisionBundle {
350 context_id: id.to_string(),
351 context_name: name.to_string(),
352 vta_url: config.public_url,
353 vta_did: config.community_vta_did,
354 credential: cred_resp.credential,
355 admin_did: cred_resp.did,
356 did: provisioned_did,
357 };
358
359 let encoded = bundle.encode().map_err(|e| format!("{e}"))?;
360
361 eprintln!();
363 eprintln!("\x1b[1;33m╔══════════════════════════════════════════════════════════════╗");
364 eprintln!("║ Context provision bundle (contains secrets — save securely) ║");
365 eprintln!("╚══════════════════════════════════════════════════════════════╝\x1b[0m");
366 eprintln!();
367 eprintln!(" Context: {} ({})", id, name);
368 eprintln!(" Admin DID: {}", bundle.admin_did);
369 if let Some(ref did) = bundle.did {
370 eprintln!(" DID: {}", did.id);
371 if did.log_entry.is_some() {
372 eprintln!(" (includes log entry for self-hosting)");
373 }
374 }
375 eprintln!();
376 println!("{encoded}");
377 eprintln!();
378
379 Ok(())
380}
381
382async fn credential_from_key(
384 client: &VtaClient,
385 key_id: &str,
386 vta_did: &str,
387 vta_url: Option<&str>,
388) -> Result<(String, String), Box<dyn std::error::Error>> {
389 let secret = client.get_key_secret(key_id).await?;
390 let seed = decode_private_key_multibase(&secret.private_key_multibase)
391 .map_err(|e| format!("Cannot decode key secret: {e}"))?;
392 let public_key = ed25519_dalek::SigningKey::from_bytes(&seed)
393 .verifying_key()
394 .to_bytes();
395 let did = format!("did:key:{}", ed25519_multibase_pubkey(&public_key));
396
397 let bundle = CredentialBundle {
398 did: did.clone(),
399 private_key_multibase: secret.private_key_multibase,
400 vta_did: vta_did.to_string(),
401 vta_url: vta_url.map(String::from),
402 };
403 let encoded = bundle.encode().map_err(|e| format!("{e}"))?;
404 Ok((encoded, did))
405}
406
407pub async fn cmd_context_reprovision(
408 client: &VtaClient,
409 id: &str,
410 key_id: Option<String>,
411 admin_label: Option<String>,
412) -> Result<(), Box<dyn std::error::Error>> {
413 eprintln!("Fetching context '{id}'...");
415 let ctx = client.get_context(id).await?;
416
417 let config = client.get_config().await?;
419 let vta_did = config
420 .community_vta_did
421 .as_deref()
422 .ok_or("VTA DID not configured")?;
423
424 let (admin_credential, admin_did) = if let Some(ref kid) = key_id {
426 eprintln!("Using key '{kid}'...");
428 credential_from_key(client, kid, vta_did, config.public_url.as_deref()).await?
429 } else {
430 let keys_resp = client.list_keys(0, 10000, Some("active"), Some(id)).await?;
432 let ed25519_keys: Vec<_> = keys_resp
433 .keys
434 .iter()
435 .filter(|k| k.key_type == KeyType::Ed25519)
436 .collect();
437
438 eprintln!();
439 eprintln!("Select an admin credential key for context '{id}':");
440 eprintln!();
441 for (i, key) in ed25519_keys.iter().enumerate() {
442 let label = key
443 .label
444 .as_deref()
445 .map(|l| format!(" ({l})"))
446 .unwrap_or_default();
447 eprintln!(" [{}] {}{}", i + 1, key.key_id, label);
448 }
449 let new_option = ed25519_keys.len() + 1;
450 eprintln!(" [{}] Create a new admin key", new_option);
451 eprintln!();
452 eprint!("Choice [{}]: ", new_option);
453 io::stderr().flush()?;
454
455 let mut input = String::new();
456 io::stdin().read_line(&mut input)?;
457 let input = input.trim();
458
459 let choice: usize = if input.is_empty() {
461 new_option
462 } else {
463 input
464 .parse()
465 .map_err(|_| format!("Invalid choice: {input}"))?
466 };
467
468 if choice == new_option {
469 eprintln!("Creating new admin key...");
471 let key_resp = client
472 .create_key(CreateKeyRequest {
473 key_type: KeyType::Ed25519,
474 derivation_path: None,
475 key_id: None,
476 mnemonic: None,
477 label: admin_label.or_else(|| Some("admin".to_string())),
478 context_id: Some(id.to_string()),
479 })
480 .await?;
481 credential_from_key(
482 client,
483 &key_resp.key_id,
484 vta_did,
485 config.public_url.as_deref(),
486 )
487 .await?
488 } else if choice >= 1 && choice <= ed25519_keys.len() {
489 let selected = &ed25519_keys[choice - 1];
490 eprintln!("Using key '{}'...", selected.key_id);
491 credential_from_key(
492 client,
493 &selected.key_id,
494 vta_did,
495 config.public_url.as_deref(),
496 )
497 .await?
498 } else {
499 return Err(format!("Invalid choice: {choice}").into());
500 }
501 };
502
503 if client.get_acl(&admin_did).await.is_err() {
505 eprintln!("Creating ACL entry for {admin_did}...");
506 client
507 .create_acl(
508 vta_sdk::client::CreateAclRequest::new(&admin_did, "admin")
509 .contexts(vec![id.to_string()]),
510 )
511 .await?;
512 }
513
514 let provisioned_did = if let Some(ref did_id) = ctx.did {
516 eprintln!("Fetching DID material...");
517
518 let log_resp = client.get_did_webvh_log(did_id).await?;
520 let (did_document, log_entry) = if let Some(ref log_str) = log_resp.log {
521 let parsed: serde_json::Value = serde_json::from_str(log_str)
522 .map_err(|e| format!("failed to parse DID log: {e}"))?;
523 let doc = parsed.get("state").cloned();
524 (doc, Some(log_str.clone()))
525 } else {
526 (None, None)
527 };
528
529 let secrets_bundle = client.fetch_did_secrets_bundle(id).await?;
531
532 Some(ProvisionedDid {
533 id: did_id.clone(),
534 did_document,
535 log_entry,
536 secrets: secrets_bundle.secrets,
537 })
538 } else {
539 None
540 };
541
542 let bundle = ContextProvisionBundle {
544 context_id: id.to_string(),
545 context_name: ctx.name.clone(),
546 vta_url: config.public_url,
547 vta_did: config.community_vta_did,
548 credential: admin_credential,
549 admin_did,
550 did: provisioned_did,
551 };
552
553 let encoded = bundle.encode().map_err(|e| format!("{e}"))?;
554
555 eprintln!();
557 eprintln!("\x1b[1;33m╔══════════════════════════════════════════════════════════════╗");
558 eprintln!("║ Context provision bundle (contains secrets — save securely) ║");
559 eprintln!("╚══════════════════════════════════════════════════════════════╝\x1b[0m");
560 eprintln!();
561 eprintln!(" Context: {} ({})", id, ctx.name);
562 eprintln!(" Admin DID: {}", bundle.admin_did);
563 if let Some(ref did) = bundle.did {
564 eprintln!(" DID: {}", did.id);
565 }
566 eprintln!();
567 println!("{encoded}");
568 eprintln!();
569
570 Ok(())
571}