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