1use vta_sdk::client::VtaClient;
18use vta_sdk::error::VtaError;
19use vta_sdk::protocol::services::{
20 DisableRestRequest, DisableTspRequest, EnableRestRequest, EnableTspRequest,
21 RollbackDidcommRequest, RollbackRestRequest, RollbackTspRequest, UpdateRestRequest,
22 UpdateTspRequest,
23};
24use vta_sdk::protocol::{
25 DisableDidcommRequest, EnableDidcommConflictBody, EnableDidcommRequest, UpdateDidcommRequest,
26};
27
28use crate::display::{NAME_HEADER, NameBook, UNNAMED, book_from_acl, inline, shorten_did};
29
30pub async fn cmd_services_list(client: &VtaClient) -> Result<(), Box<dyn std::error::Error>> {
34 let response = client.list_services().await?;
35
36 println!("Services advertised on this VTA's DID document:");
37 println!();
38 for state in &response.services {
39 match state {
40 vta_sdk::protocol::services::ServiceState::Tsp {
41 enabled,
42 mediator_did,
43 } => {
44 let on = if *enabled { "on" } else { "off" };
45 println!(" TSP: {on}");
46 if let Some(m) = mediator_did {
47 println!(" Mediator: {m}");
48 }
49 }
50 vta_sdk::protocol::services::ServiceState::Didcomm {
51 enabled,
52 mediator_did,
53 routing_keys,
54 } => {
55 let on = if *enabled { "on" } else { "off" };
56 println!(" DIDComm: {on}");
57 if let Some(m) = mediator_did {
58 println!(" Mediator: {m}");
59 }
60 if !routing_keys.is_empty() {
61 println!(" Routing keys: {}", routing_keys.join(", "));
62 }
63 }
64 vta_sdk::protocol::services::ServiceState::Rest { enabled, url } => {
65 let on = if *enabled { "on" } else { "off" };
66 println!(" REST: {on}");
67 if let Some(u) = url {
68 println!(" URL: {u}");
69 }
70 }
71 vta_sdk::protocol::services::ServiceState::Webauthn { enabled, url } => {
72 let on = if *enabled { "on" } else { "off" };
73 println!(" WebAuthn: {on}");
74 if let Some(u) = url {
75 println!(" URL: {u}");
76 }
77 }
78 }
79 }
80 Ok(())
81}
82
83pub async fn cmd_services_rest_enable(
86 client: &VtaClient,
87 url: String,
88) -> Result<(), Box<dyn std::error::Error>> {
89 let req = EnableRestRequest::new(url);
90 let resp = client.enable_rest(req).await?;
91 println!("REST enabled.");
92 println!(" New version ID: {}", resp.log_entry_version_id);
93 println!(" Effective at: {}", resp.effective_at);
94 print_serverless_hint(resp.serverless, &resp.vta_did);
95 Ok(())
96}
97
98pub async fn cmd_services_rest_update(
99 client: &VtaClient,
100 url: String,
101) -> Result<(), Box<dyn std::error::Error>> {
102 let req = UpdateRestRequest::new(url);
103 let resp = client.update_rest(req).await?;
104 println!("REST URL updated.");
105 println!(" New version ID: {}", resp.log_entry_version_id);
106 println!(" Effective at: {}", resp.effective_at);
107 print_serverless_hint(resp.serverless, &resp.vta_did);
108 Ok(())
109}
110
111pub async fn cmd_services_rest_disable(
112 client: &VtaClient,
113) -> Result<(), Box<dyn std::error::Error>> {
114 let resp = client.disable_rest(DisableRestRequest::default()).await?;
115 println!("REST disabled.");
116 println!(" New version ID: {}", resp.log_entry_version_id);
117 println!(" Effective at: {}", resp.effective_at);
118 print_serverless_hint(resp.serverless, &resp.vta_did);
119 Ok(())
120}
121
122pub async fn cmd_services_rest_rollback(
123 client: &VtaClient,
124) -> Result<(), Box<dyn std::error::Error>> {
125 let resp = client.rollback_rest(RollbackRestRequest::default()).await?;
126 print_rollback_result("REST", &resp);
127 Ok(())
128}
129
130pub async fn cmd_services_tsp_enable(
138 client: &VtaClient,
139 mediator_did: String,
140) -> Result<(), Box<dyn std::error::Error>> {
141 let req = EnableTspRequest::new(mediator_did);
142 let resp = client.enable_tsp(req).await?;
143 println!("TSP enabled.");
144 println!(" New version ID: {}", resp.log_entry_version_id);
145 println!(" Effective at: {}", resp.effective_at);
146 print_serverless_hint(resp.serverless, &resp.vta_did);
147 Ok(())
148}
149
150pub async fn cmd_services_tsp_update(
151 client: &VtaClient,
152 mediator_did: String,
153) -> Result<(), Box<dyn std::error::Error>> {
154 let req = UpdateTspRequest::new(mediator_did);
155 let resp = client.update_tsp(req).await?;
156 println!("TSP mediator DID updated.");
157 println!(" New version ID: {}", resp.log_entry_version_id);
158 println!(" Effective at: {}", resp.effective_at);
159 print_serverless_hint(resp.serverless, &resp.vta_did);
160 Ok(())
161}
162
163pub async fn cmd_services_tsp_disable(
164 client: &VtaClient,
165) -> Result<(), Box<dyn std::error::Error>> {
166 let resp = client.disable_tsp(DisableTspRequest::default()).await?;
167 println!("TSP disabled.");
168 println!(" New version ID: {}", resp.log_entry_version_id);
169 println!(" Effective at: {}", resp.effective_at);
170 print_serverless_hint(resp.serverless, &resp.vta_did);
171 Ok(())
172}
173
174pub async fn cmd_services_tsp_rollback(
175 client: &VtaClient,
176) -> Result<(), Box<dyn std::error::Error>> {
177 let resp = client.rollback_tsp(RollbackTspRequest::default()).await?;
178 print_rollback_result("TSP", &resp);
179 Ok(())
180}
181
182pub async fn cmd_services_webauthn_enable(
185 client: &VtaClient,
186 url: String,
187) -> Result<(), Box<dyn std::error::Error>> {
188 let req = vta_sdk::protocol::services::EnableWebauthnRequest::new(url);
189 let resp = client.enable_webauthn(req).await?;
190 println!("WebAuthn enabled.");
191 println!(" New version ID: {}", resp.log_entry_version_id);
192 println!(" Effective at: {}", resp.effective_at);
193 print_serverless_hint(resp.serverless, &resp.vta_did);
194 Ok(())
195}
196
197pub async fn cmd_services_webauthn_update(
198 client: &VtaClient,
199 url: String,
200) -> Result<(), Box<dyn std::error::Error>> {
201 let req = vta_sdk::protocol::services::UpdateWebauthnRequest::new(url);
202 let resp = client.update_webauthn(req).await?;
203 println!("WebAuthn URL updated.");
204 println!(" New version ID: {}", resp.log_entry_version_id);
205 println!(" Effective at: {}", resp.effective_at);
206 print_serverless_hint(resp.serverless, &resp.vta_did);
207 Ok(())
208}
209
210pub async fn cmd_services_webauthn_disable(
211 client: &VtaClient,
212) -> Result<(), Box<dyn std::error::Error>> {
213 eprintln!(
214 "WARNING: disabling WebAuthn will also REMOVE passkey verificationMethods from every DID \
215 this VTA controls. Any operator currently using passkey login will need to re-enrol \
216 after the next `services webauthn enable`."
217 );
218 let resp = client
219 .disable_webauthn(vta_sdk::protocol::services::DisableWebauthnRequest::default())
220 .await?;
221 println!("WebAuthn disabled.");
222 println!(" New version ID: {}", resp.log_entry_version_id);
223 println!(" Effective at: {}", resp.effective_at);
224 print_serverless_hint(resp.serverless, &resp.vta_did);
225 Ok(())
226}
227
228pub async fn cmd_services_webauthn_rollback(
229 client: &VtaClient,
230) -> Result<(), Box<dyn std::error::Error>> {
231 let resp = client
232 .rollback_webauthn(vta_sdk::protocol::services::RollbackWebauthnRequest::default())
233 .await?;
234 print_rollback_result("WebAuthn", &resp);
235 Ok(())
236}
237
238pub async fn cmd_services_didcomm_enable(
241 client: &VtaClient,
242 mediator_did: String,
243 force: bool,
244 handshake_timeout_secs: Option<u64>,
245) -> Result<(), Box<dyn std::error::Error>> {
246 let mut req = EnableDidcommRequest::new(&mediator_did);
247 req.force = force;
248 req.handshake_timeout_secs = handshake_timeout_secs;
249 let resp = match client.enable_didcomm(req).await {
250 Ok(resp) => resp,
251 Err(VtaError::Conflict(body)) => {
252 if let Ok(conflict) = serde_json::from_str::<EnableDidcommConflictBody>(&body)
253 && conflict.error == "didcomm_already_enabled"
254 {
255 println!("DIDComm already enabled.");
256 if let Some(mediator_did) = conflict.mediator_did {
257 println!(" Mediator DID: {mediator_did}");
258 }
259 return Ok(());
260 }
261 return Err(VtaError::Conflict(body).into());
262 }
263 Err(e) => return Err(e.into()),
264 };
265 println!("DIDComm enabled.");
266 println!(" Mediator DID: {}", resp.mediator_did);
267 if !resp.mediator_endpoint.is_empty() {
268 println!(" Mediator URL: {}", resp.mediator_endpoint);
269 }
270 println!(" New version ID: {}", resp.new_version_id);
271 if force {
272 println!();
273 println!(" Note: --force was set; mediator handshake steps 2-5 were bypassed.");
274 }
275 print_serverless_hint(resp.serverless, &resp.vta_did);
276 Ok(())
277}
278
279pub async fn cmd_services_didcomm_update(
280 client: &VtaClient,
281 new_mediator_did: String,
282 drain_ttl_secs: u64,
283 force: bool,
284 handshake_timeout_secs: Option<u64>,
285) -> Result<(), Box<dyn std::error::Error>> {
286 let mut req = UpdateDidcommRequest::new(&new_mediator_did, drain_ttl_secs);
287 req.force = force;
288 req.handshake_timeout_secs = handshake_timeout_secs;
289 let resp = client.update_didcomm(req).await?;
290 println!("DIDComm mediator updated.");
291 println!(" Prior mediator: {}", resp.prior_mediator_did);
292 println!(" Active mediator: {}", resp.active_mediator_did);
293 if !resp.active_mediator_endpoint.is_empty() {
294 println!(" Active endpoint: {}", resp.active_mediator_endpoint);
295 }
296 println!(" New version ID: {}", resp.new_version_id);
297 println!(
298 " Drain deadline: {} (prior listener stays up until then)",
299 resp.drains_until
300 );
301 print_serverless_hint(resp.serverless, &resp.vta_did);
302 Ok(())
303}
304
305pub async fn cmd_services_didcomm_disable(
306 client: &VtaClient,
307 drain_ttl_secs: u64,
308) -> Result<(), Box<dyn std::error::Error>> {
309 let req = DisableDidcommRequest::new(drain_ttl_secs);
310 let resp = client.disable_didcomm(req).await?;
311 println!("DIDComm disabled.");
312 println!(" Prior mediator: {}", resp.prior_mediator_did);
313 println!(" New version ID: {}", resp.new_version_id);
314 match resp.drains_until {
315 Some(deadline) => {
316 println!(" Drain deadline: {deadline}");
317 println!();
318 println!(" The listener stays up until the deadline so in-flight messages can drain.");
319 println!(
320 " Cancel early with `pnm services didcomm drain cancel --mediator-did <did>`."
321 );
322 }
323 None => println!(" Listener torn down immediately (drain TTL was 0)."),
324 }
325 print_serverless_hint(resp.serverless, &resp.vta_did);
326 Ok(())
327}
328
329pub async fn cmd_services_didcomm_rollback(
330 client: &VtaClient,
331 drain_ttl_secs: Option<u64>,
332) -> Result<(), Box<dyn std::error::Error>> {
333 let req = RollbackDidcommRequest { drain_ttl_secs };
334 let resp = client.rollback_didcomm(req).await?;
335 print_rollback_result("DIDComm", &resp);
336 Ok(())
337}
338
339async fn mediator_name_book(client: &VtaClient) -> NameBook {
349 let mut book = NameBook::new();
350 if let Ok(acl) = client.list_acl(None).await {
351 book_from_acl(&mut book, &acl.entries);
352 }
353 book
354}
355
356pub async fn cmd_services_didcomm_drain_list(
359 client: &VtaClient,
360) -> Result<(), Box<dyn std::error::Error>> {
361 let resp = client.list_drain().await?;
362 if resp.entries.is_empty() {
363 println!("No mediators currently in drain.");
364 return Ok(());
365 }
366 println!("Drain set ({} mediator(s)):", resp.entries.len());
367 println!();
368
369 let book = mediator_name_book(client).await;
370 let show_names = book.names_any(resp.entries.iter().map(|e| e.mediator_did.as_str()));
371
372 let header_did = "MEDIATOR DID";
373 let header_until = "DRAIN UNTIL";
374 if show_names {
375 println!(" {:<24} {header_did:<46} {header_until}", NAME_HEADER);
376 } else {
377 println!(" {header_did:<46} {header_until}");
378 }
379 for e in &resp.entries {
380 let did = shorten_did(&e.mediator_did);
381 if show_names {
382 let name = book
383 .name_of(&e.mediator_did)
384 .unwrap_or_else(|| UNNAMED.into());
385 println!(" {:<24} {:<46} {}", name, did, e.drains_until);
386 } else {
387 println!(" {:<46} {}", did, e.drains_until);
388 }
389 }
390 Ok(())
391}
392
393pub async fn cmd_services_didcomm_drain_cancel(
394 client: &VtaClient,
395 mediator_did: String,
396) -> Result<(), Box<dyn std::error::Error>> {
397 let req = vta_sdk::protocol::DrainCancelRequest { mediator_did };
398 let resp = client.drain_cancel(req).await?;
399 println!("Drain cancelled for {}.", resp.mediator_did);
400 println!(" Listener was torn down immediately.");
401 Ok(())
402}
403
404pub async fn cmd_services_report(
407 client: &VtaClient,
408 since: Option<String>,
409 until: Option<String>,
410 format: ReportFormat,
411) -> Result<(), Box<dyn std::error::Error>> {
412 let report = client
413 .mediator_report(since.as_deref(), until.as_deref())
414 .await?;
415
416 match format {
417 ReportFormat::Json => {
418 println!("{}", serde_json::to_string_pretty(&report)?);
419 }
420 ReportFormat::Table => {
421 println!("Service-management report");
422 if let Some(ref s) = report.since {
423 println!(" Window: {s} → {}", report.until);
424 } else {
425 println!(" Window: (all time) → {}", report.until);
426 }
427 println!();
428 let book = mediator_name_book(client).await;
432
433 if report.mediators.is_empty() {
434 println!(" No inbound DIDComm messages recorded.");
435 } else {
436 println!(" Per-mediator inbound counts (most recent first):");
437 let show_names =
438 book.names_any(report.mediators.iter().map(|m| m.mediator_did.as_str()));
439 let header_did = "MEDIATOR DID";
440 let header_count = "INBOUND";
441 if show_names {
442 println!(
443 " {:<24} {header_did:<46} {header_count:>10} LAST SEEN",
444 NAME_HEADER
445 );
446 } else {
447 println!(" {header_did:<46} {header_count:>10} LAST SEEN");
448 }
449 for m in &report.mediators {
450 let did = shorten_did(&m.mediator_did);
451 if show_names {
452 let name = book
453 .name_of(&m.mediator_did)
454 .unwrap_or_else(|| UNNAMED.into());
455 println!(
456 " {:<24} {:<46} {:>10} {}",
457 name, did, m.inbound_count, m.last_seen
458 );
459 } else {
460 println!(" {:<46} {:>10} {}", did, m.inbound_count, m.last_seen);
461 }
462 }
463 }
464 if !report.senders.is_empty() {
465 println!();
466 println!(" Senders by last-seen mediator:");
467 for s in &report.senders {
468 println!(
471 " {} → {} (at {})",
472 inline(&book, &s.sender_did),
473 inline(&book, &s.last_seen_mediator),
474 s.last_seen_at
475 );
476 }
477 }
478 }
479 }
480 Ok(())
481}
482
483fn print_rollback_result(kind: &str, resp: &vta_sdk::protocol::services::RollbackResponse) {
486 if resp.kind == "no_op" {
487 println!("{kind} rollback: no change required.");
488 println!(" Snapshot matches current state — nothing to do.");
489 return;
490 }
491 println!("{kind} rolled back.");
492 println!(" Action: {}", resp.kind);
493 if !resp.log_entry_version_id.is_empty() {
494 println!(" New version ID: {}", resp.log_entry_version_id);
495 }
496 println!(" Effective at: {}", resp.effective_at);
497 if let Some(ref drain_until) = resp.drain_until {
498 println!(" Drain deadline: {drain_until}");
499 }
500 if let Some(ref draining) = resp.draining_mediator {
501 println!(" Draining: {draining}");
502 }
503 print_serverless_hint(resp.serverless, &resp.vta_did);
504}
505
506pub fn print_serverless_hint(serverless: bool, vta_did: &str) {
519 if !serverless || vta_did.is_empty() {
520 return;
521 }
522 println!();
523 println!(" This VTA's DID is self-hosted. Fetch the updated log:");
524 println!(" pnm did-mgmt dids get-log {vta_did} --out did.jsonl");
525 println!(" then redeploy did.jsonl to your host. Until you do,");
526 println!(" resolvers will keep returning the prior version.");
527}
528
529#[derive(Debug, Clone, Copy)]
530pub enum ReportFormat {
531 Json,
532 Table,
533}
534
535impl std::str::FromStr for ReportFormat {
536 type Err = String;
537 fn from_str(s: &str) -> Result<Self, Self::Err> {
538 match s {
539 "json" => Ok(Self::Json),
540 "table" => Ok(Self::Table),
541 other => Err(format!("unknown format `{other}` — use `json` or `table`")),
542 }
543 }
544}