1use vta_sdk::client::VtaClient;
18use vta_sdk::protocol::services::{
19 DisableRestRequest, EnableRestRequest, RollbackDidcommRequest, RollbackRestRequest,
20 UpdateRestRequest,
21};
22use vta_sdk::protocol::{DisableDidcommRequest, EnableDidcommRequest, UpdateDidcommRequest};
23
24pub async fn cmd_services_list(client: &VtaClient) -> Result<(), Box<dyn std::error::Error>> {
28 let response = client.list_services().await?;
29
30 println!("Services advertised on this VTA's DID document:");
31 println!();
32 for state in &response.services {
33 match state {
34 vta_sdk::protocol::services::ServiceState::Didcomm {
35 enabled,
36 mediator_did,
37 routing_keys,
38 } => {
39 let on = if *enabled { "on" } else { "off" };
40 println!(" DIDComm: {on}");
41 if let Some(m) = mediator_did {
42 println!(" Mediator: {m}");
43 }
44 if !routing_keys.is_empty() {
45 println!(" Routing keys: {}", routing_keys.join(", "));
46 }
47 }
48 vta_sdk::protocol::services::ServiceState::Rest { enabled, url } => {
49 let on = if *enabled { "on" } else { "off" };
50 println!(" REST: {on}");
51 if let Some(u) = url {
52 println!(" URL: {u}");
53 }
54 }
55 }
56 }
57 Ok(())
58}
59
60pub async fn cmd_services_rest_enable(
63 client: &VtaClient,
64 url: String,
65) -> Result<(), Box<dyn std::error::Error>> {
66 let req = EnableRestRequest::new(url);
67 let resp = client.enable_rest(req).await?;
68 println!("REST enabled.");
69 println!(" New version ID: {}", resp.log_entry_version_id);
70 println!(" Effective at: {}", resp.effective_at);
71 print_serverless_hint(resp.serverless, &resp.vta_did);
72 Ok(())
73}
74
75pub async fn cmd_services_rest_update(
76 client: &VtaClient,
77 url: String,
78) -> Result<(), Box<dyn std::error::Error>> {
79 let req = UpdateRestRequest::new(url);
80 let resp = client.update_rest(req).await?;
81 println!("REST URL updated.");
82 println!(" New version ID: {}", resp.log_entry_version_id);
83 println!(" Effective at: {}", resp.effective_at);
84 print_serverless_hint(resp.serverless, &resp.vta_did);
85 Ok(())
86}
87
88pub async fn cmd_services_rest_disable(
89 client: &VtaClient,
90) -> Result<(), Box<dyn std::error::Error>> {
91 let resp = client.disable_rest(DisableRestRequest::default()).await?;
92 println!("REST disabled.");
93 println!(" New version ID: {}", resp.log_entry_version_id);
94 println!(" Effective at: {}", resp.effective_at);
95 print_serverless_hint(resp.serverless, &resp.vta_did);
96 Ok(())
97}
98
99pub async fn cmd_services_rest_rollback(
100 client: &VtaClient,
101) -> Result<(), Box<dyn std::error::Error>> {
102 let resp = client.rollback_rest(RollbackRestRequest::default()).await?;
103 print_rollback_result("REST", &resp);
104 Ok(())
105}
106
107pub async fn cmd_services_didcomm_enable(
110 client: &VtaClient,
111 mediator_did: String,
112 force: bool,
113 handshake_timeout_secs: Option<u64>,
114) -> Result<(), Box<dyn std::error::Error>> {
115 let mut req = EnableDidcommRequest::new(&mediator_did);
116 req.force = force;
117 req.handshake_timeout_secs = handshake_timeout_secs;
118 let resp = client.enable_didcomm(req).await?;
119 println!("DIDComm enabled.");
120 println!(" Mediator DID: {}", resp.mediator_did);
121 if !resp.mediator_endpoint.is_empty() {
122 println!(" Mediator URL: {}", resp.mediator_endpoint);
123 }
124 println!(" New version ID: {}", resp.new_version_id);
125 if force {
126 println!();
127 println!(" Note: --force was set; mediator handshake steps 2-5 were bypassed.");
128 }
129 print_serverless_hint(resp.serverless, &resp.vta_did);
130 Ok(())
131}
132
133pub async fn cmd_services_didcomm_update(
134 client: &VtaClient,
135 new_mediator_did: String,
136 drain_ttl_secs: u64,
137 force: bool,
138 handshake_timeout_secs: Option<u64>,
139) -> Result<(), Box<dyn std::error::Error>> {
140 let mut req = UpdateDidcommRequest::new(&new_mediator_did, drain_ttl_secs);
141 req.force = force;
142 req.handshake_timeout_secs = handshake_timeout_secs;
143 let resp = client.update_didcomm(req).await?;
144 println!("DIDComm mediator updated.");
145 println!(" Prior mediator: {}", resp.prior_mediator_did);
146 println!(" Active mediator: {}", resp.active_mediator_did);
147 if !resp.active_mediator_endpoint.is_empty() {
148 println!(" Active endpoint: {}", resp.active_mediator_endpoint);
149 }
150 println!(" New version ID: {}", resp.new_version_id);
151 println!(
152 " Drain deadline: {} (prior listener stays up until then)",
153 resp.drains_until
154 );
155 print_serverless_hint(resp.serverless, &resp.vta_did);
156 Ok(())
157}
158
159pub async fn cmd_services_didcomm_disable(
160 client: &VtaClient,
161 drain_ttl_secs: u64,
162) -> Result<(), Box<dyn std::error::Error>> {
163 let req = DisableDidcommRequest::new(drain_ttl_secs);
164 let resp = client.disable_didcomm(req).await?;
165 println!("DIDComm disabled.");
166 println!(" Prior mediator: {}", resp.prior_mediator_did);
167 println!(" New version ID: {}", resp.new_version_id);
168 match resp.drains_until {
169 Some(deadline) => {
170 println!(" Drain deadline: {deadline}");
171 println!();
172 println!(" The listener stays up until the deadline so in-flight messages can drain.");
173 println!(
174 " Cancel early with `pnm services didcomm drain cancel --mediator-did <did>`."
175 );
176 }
177 None => println!(" Listener torn down immediately (drain TTL was 0)."),
178 }
179 print_serverless_hint(resp.serverless, &resp.vta_did);
180 Ok(())
181}
182
183pub async fn cmd_services_didcomm_rollback(
184 client: &VtaClient,
185 drain_ttl_secs: Option<u64>,
186) -> Result<(), Box<dyn std::error::Error>> {
187 let req = RollbackDidcommRequest { drain_ttl_secs };
188 let resp = client.rollback_didcomm(req).await?;
189 print_rollback_result("DIDComm", &resp);
190 Ok(())
191}
192
193pub async fn cmd_services_didcomm_drain_list(
196 client: &VtaClient,
197) -> Result<(), Box<dyn std::error::Error>> {
198 let resp = client.list_drain().await?;
199 if resp.entries.is_empty() {
200 println!("No mediators currently in drain.");
201 return Ok(());
202 }
203 println!("Drain set ({} mediator(s)):", resp.entries.len());
204 println!();
205 let header_did = "MEDIATOR DID";
206 let header_until = "DRAIN UNTIL";
207 println!(" {header_did:<60} {header_until}");
208 for e in &resp.entries {
209 println!(
210 " {:<60} {}",
211 truncate(&e.mediator_did, 60),
212 e.drains_until
213 );
214 }
215 Ok(())
216}
217
218pub async fn cmd_services_didcomm_drain_cancel(
219 client: &VtaClient,
220 mediator_did: String,
221) -> Result<(), Box<dyn std::error::Error>> {
222 let req = vta_sdk::protocol::DrainCancelRequest { mediator_did };
223 let resp = client.drain_cancel(req).await?;
224 println!("Drain cancelled for {}.", resp.mediator_did);
225 println!(" Listener was torn down immediately.");
226 Ok(())
227}
228
229pub async fn cmd_services_report(
232 client: &VtaClient,
233 since: Option<String>,
234 until: Option<String>,
235 format: ReportFormat,
236) -> Result<(), Box<dyn std::error::Error>> {
237 let report = client
238 .mediator_report(since.as_deref(), until.as_deref())
239 .await?;
240
241 match format {
242 ReportFormat::Json => {
243 println!("{}", serde_json::to_string_pretty(&report)?);
244 }
245 ReportFormat::Table => {
246 println!("Service-management report");
247 if let Some(ref s) = report.since {
248 println!(" Window: {s} → {}", report.until);
249 } else {
250 println!(" Window: (all time) → {}", report.until);
251 }
252 println!();
253 if report.mediators.is_empty() {
254 println!(" No inbound DIDComm messages recorded.");
255 } else {
256 println!(" Per-mediator inbound counts (most recent first):");
257 let header_did = "MEDIATOR DID";
258 let header_count = "INBOUND";
259 println!(" {header_did:<60} {header_count:>10} LAST SEEN");
260 for m in &report.mediators {
261 println!(
262 " {:<60} {:>10} {}",
263 truncate(&m.mediator_did, 60),
264 m.inbound_count,
265 m.last_seen
266 );
267 }
268 }
269 if !report.senders.is_empty() {
270 println!();
271 println!(" Senders by last-seen mediator:");
272 for s in &report.senders {
273 println!(
274 " {} → {} (at {})",
275 truncate(&s.sender_did, 50),
276 truncate(&s.last_seen_mediator, 50),
277 s.last_seen_at
278 );
279 }
280 }
281 }
282 }
283 Ok(())
284}
285
286fn print_rollback_result(kind: &str, resp: &vta_sdk::protocol::services::RollbackResponse) {
289 if resp.kind == "no_op" {
290 println!("{kind} rollback: no change required.");
291 println!(" Snapshot matches current state — nothing to do.");
292 return;
293 }
294 println!("{kind} rolled back.");
295 println!(" Action: {}", resp.kind);
296 if !resp.log_entry_version_id.is_empty() {
297 println!(" New version ID: {}", resp.log_entry_version_id);
298 }
299 println!(" Effective at: {}", resp.effective_at);
300 if let Some(ref drain_until) = resp.drain_until {
301 println!(" Drain deadline: {drain_until}");
302 }
303 if let Some(ref draining) = resp.draining_mediator {
304 println!(" Draining: {draining}");
305 }
306 print_serverless_hint(resp.serverless, &resp.vta_did);
307}
308
309pub fn print_serverless_hint(serverless: bool, vta_did: &str) {
322 if !serverless || vta_did.is_empty() {
323 return;
324 }
325 println!();
326 println!(" This VTA's DID is self-hosted. Fetch the updated log:");
327 println!(" pnm webvh did-log {vta_did} --out did.jsonl");
328 println!(" then redeploy did.jsonl to your host. Until you do,");
329 println!(" resolvers will keep returning the prior version.");
330}
331
332#[derive(Debug, Clone, Copy)]
333pub enum ReportFormat {
334 Json,
335 Table,
336}
337
338impl std::str::FromStr for ReportFormat {
339 type Err = String;
340 fn from_str(s: &str) -> Result<Self, Self::Err> {
341 match s {
342 "json" => Ok(Self::Json),
343 "table" => Ok(Self::Table),
344 other => Err(format!("unknown format `{other}` — use `json` or `table`")),
345 }
346 }
347}
348
349fn truncate(s: &str, max: usize) -> String {
350 if s.chars().count() <= max {
351 s.to_string()
352 } else {
353 let mut out: String = s.chars().take(max - 1).collect();
354 out.push('…');
355 out
356 }
357}