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