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