Skip to main content

vta_cli_common/commands/
mediator.rs

1//! `pnm mediator …` command implementations.
2//!
3//! Spec: `docs/05-design-notes/didcomm-protocol-management.md`.
4//!
5//! Phase 4 lands `migrate` and the `rollback` alias. `drain cancel`
6//! and `report` arrive in P4.3 / P4.4.
7
8use vta_sdk::client::VtaClient;
9use vta_sdk::protocol::{DrainCancelRequest, MigrateMediatorRequest};
10
11/// `pnm mediator migrate --to <did> --drain-ttl <secs> [--force]
12///                       [--handshake-timeout <secs>]`.
13pub async fn cmd_mediator_migrate(
14    client: &VtaClient,
15    new_mediator_did: String,
16    drain_ttl_secs: u64,
17    force: bool,
18    handshake_timeout_secs: Option<u64>,
19) -> Result<(), Box<dyn std::error::Error>> {
20    run_migrate(
21        client,
22        new_mediator_did,
23        drain_ttl_secs,
24        force,
25        handshake_timeout_secs,
26        /* rollback = */ false,
27    )
28    .await
29}
30
31/// `pnm mediator rollback --to <did> --drain-ttl <secs>`.
32///
33/// Mechanically identical to `migrate` but tagged in telemetry as
34/// a rollback so reports can distinguish forward and reverse moves.
35/// Spec criterion #6 (rollback equivalence): the resulting DID doc
36/// matches the pre-migrate state byte-for-byte in `service[]`,
37/// modulo `versionId` and the rotated WebVH control keys.
38pub async fn cmd_mediator_rollback(
39    client: &VtaClient,
40    target_mediator_did: String,
41    drain_ttl_secs: u64,
42    force: bool,
43    handshake_timeout_secs: Option<u64>,
44) -> Result<(), Box<dyn std::error::Error>> {
45    run_migrate(
46        client,
47        target_mediator_did,
48        drain_ttl_secs,
49        force,
50        handshake_timeout_secs,
51        /* rollback = */ true,
52    )
53    .await
54}
55
56async fn run_migrate(
57    client: &VtaClient,
58    new_mediator_did: String,
59    drain_ttl_secs: u64,
60    force: bool,
61    handshake_timeout_secs: Option<u64>,
62    rollback: bool,
63) -> Result<(), Box<dyn std::error::Error>> {
64    let mut req = MigrateMediatorRequest::new(&new_mediator_did, drain_ttl_secs);
65    req.force = force;
66    req.handshake_timeout_secs = handshake_timeout_secs;
67    req.rollback = rollback;
68
69    let resp = client
70        .migrate_mediator(req)
71        .await
72        .map_err(|e| format!("{e}"))?;
73
74    let verb = if rollback { "rolled back" } else { "migrated" };
75    println!("Mediator {verb}.");
76    println!("  Prior mediator:  {}", resp.prior_mediator_did);
77    println!("  Active mediator: {}", resp.active_mediator_did);
78    if !resp.active_mediator_endpoint.is_empty() {
79        println!("  Active endpoint: {}", resp.active_mediator_endpoint);
80    }
81    println!("  New version ID:  {}", resp.new_version_id);
82    println!(
83        "  Drain deadline:  {} (prior listener stays up until then)",
84        resp.drains_until
85    );
86    if force {
87        println!();
88        println!("  Note: --force was set; mediator handshake steps 2-5 were bypassed.");
89    }
90    Ok(())
91}
92
93/// `pnm mediator drain cancel --mediator-did <did>`.
94pub async fn cmd_mediator_drain_cancel(
95    client: &VtaClient,
96    mediator_did: String,
97) -> Result<(), Box<dyn std::error::Error>> {
98    let req = DrainCancelRequest { mediator_did };
99    let resp = client.drain_cancel(req).await.map_err(|e| format!("{e}"))?;
100    println!("Drain cancelled for {}.", resp.mediator_did);
101    println!("  Listener was torn down immediately.");
102    Ok(())
103}
104
105/// `pnm mediator report [--since <rfc3339>] [--until <rfc3339>]
106///                      [--format json|table]`.
107pub async fn cmd_mediator_report(
108    client: &VtaClient,
109    since: Option<String>,
110    until: Option<String>,
111    format: ReportFormat,
112) -> Result<(), Box<dyn std::error::Error>> {
113    let report = client
114        .mediator_report(since.as_deref(), until.as_deref())
115        .await
116        .map_err(|e| format!("{e}"))?;
117
118    match format {
119        ReportFormat::Json => {
120            println!("{}", serde_json::to_string_pretty(&report)?);
121        }
122        ReportFormat::Table => {
123            println!("Mediator report");
124            if let Some(ref s) = report.since {
125                println!("  Window: {s} → {}", report.until);
126            } else {
127                println!("  Window: (all time) → {}", report.until);
128            }
129            println!();
130            if report.mediators.is_empty() {
131                println!("  No inbound DIDComm messages recorded.");
132            } else {
133                println!("  Per-mediator inbound counts (most recent first):");
134                let header_did = "MEDIATOR DID";
135                let header_count = "INBOUND";
136                println!("    {header_did:<60}  {header_count:>10}  LAST SEEN");
137                for m in &report.mediators {
138                    println!(
139                        "    {:<60}  {:>10}  {}",
140                        truncate(&m.mediator_did, 60),
141                        m.inbound_count,
142                        m.last_seen
143                    );
144                }
145            }
146            if !report.senders.is_empty() {
147                println!();
148                println!("  Senders by last-seen mediator:");
149                for s in &report.senders {
150                    println!(
151                        "    {} → {} (at {})",
152                        truncate(&s.sender_did, 50),
153                        truncate(&s.last_seen_mediator, 50),
154                        s.last_seen_at
155                    );
156                }
157            }
158        }
159    }
160    Ok(())
161}
162
163#[derive(Debug, Clone, Copy)]
164pub enum ReportFormat {
165    Json,
166    Table,
167}
168
169impl std::str::FromStr for ReportFormat {
170    type Err = String;
171    fn from_str(s: &str) -> Result<Self, Self::Err> {
172        match s {
173            "json" => Ok(Self::Json),
174            "table" => Ok(Self::Table),
175            other => Err(format!("unknown format `{other}` — use `json` or `table`")),
176        }
177    }
178}
179
180fn truncate(s: &str, max: usize) -> String {
181    if s.chars().count() <= max {
182        s.to_string()
183    } else {
184        let mut out: String = s.chars().take(max - 1).collect();
185        out.push('…');
186        out
187    }
188}