use ratatui::{
buffer::{Buffer, CellDiffOption},
layout::Rect,
style::{Color, Modifier},
widgets::Widget,
};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
static BIN_NAME: OnceLock<&'static str> = OnceLock::new();
pub fn set_bin_name(name: &'static str) {
let _ = BIN_NAME.set(name);
}
pub fn bin_name() -> &'static str {
BIN_NAME.get().copied().unwrap_or("vta")
}
static FULL_DISPLAY: AtomicBool = AtomicBool::new(false);
pub fn set_full_display(enabled: bool) {
FULL_DISPLAY.store(enabled, Ordering::Relaxed);
}
pub fn is_full_display() -> bool {
FULL_DISPLAY.load(Ordering::Relaxed)
}
pub fn print_truncation_hint() {
println!(
" {DIM}Identifiers are shortened to fit. Re-run with `{} --full-display …` \
to copy one in full.{RESET}",
bin_name()
);
}
pub fn print_full_entry(pairs: &[(&str, &str)]) {
let widest = pairs.iter().map(|(l, _)| l.len()).max().unwrap_or(0);
for (label, value) in pairs {
let pad = " ".repeat(widest.saturating_sub(label.len()));
println!(" {label}:{pad} {DIM}{value}{RESET}");
}
println!();
}
pub fn print_full_entry_owned(pairs: &[(&str, String)]) {
let borrowed: Vec<(&str, &str)> = pairs.iter().map(|(l, v)| (*l, v.as_str())).collect();
print_full_entry(&borrowed);
}
pub fn print_full_list_title(title: &str, count: usize) {
println!();
println!("{BOLD}{title} ({count}){RESET}");
println!();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Human,
Json,
}
static OUTPUT_FORMAT: AtomicU8 = AtomicU8::new(0);
pub fn set_output_format(format: OutputFormat) {
OUTPUT_FORMAT.store(
match format {
OutputFormat::Human => 0,
OutputFormat::Json => 1,
},
Ordering::Relaxed,
);
}
pub fn output_format() -> OutputFormat {
if OUTPUT_FORMAT.load(Ordering::Relaxed) == 1 {
OutputFormat::Json
} else {
OutputFormat::Human
}
}
#[must_use]
pub fn is_json_output() -> bool {
output_format() == OutputFormat::Json
}
pub fn print_json<T: serde::Serialize>(value: &T) -> Result<(), serde_json::Error> {
let text = serde_json::to_string_pretty(value)?;
println!("{text}");
Ok(())
}
pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m";
pub const GREEN: &str = "\x1b[32m";
pub const RED: &str = "\x1b[31m";
pub const CYAN: &str = "\x1b[36m";
pub const YELLOW: &str = "\x1b[33m";
pub const RESET: &str = "\x1b[0m";
pub fn print_cli_error(err: &(dyn std::error::Error + 'static)) {
use vta_sdk::error::VtaError;
if let Some(vta_err) = err.downcast_ref::<VtaError>() {
match vta_err {
VtaError::Auth(msg) => {
eprintln!("{RED}\u{2717}{RESET} Authentication failed: {msg}");
eprintln!(
" {DIM}Token may be expired. Try `pnm setup` to re-authenticate, or check \
that the VTA's `/auth` endpoint is reachable.{RESET}"
);
}
VtaError::Forbidden(msg) => {
eprintln!("{RED}\u{2717}{RESET} Forbidden: {msg}");
eprintln!(
" {DIM}Your role or context access doesn't permit this operation. \
Inspect with `pnm acl get <your-did>`.{RESET}"
);
}
VtaError::NotFound(msg) => {
eprintln!("{RED}\u{2717}{RESET} Not found: {msg}");
}
VtaError::Conflict(msg) => {
eprintln!(
"{RED}\u{2717}{RESET} Conflict: {}",
extract_human_message(msg)
);
}
VtaError::Gone(msg) => {
let human = extract_human_message(msg);
eprintln!("{RED}\u{2717}{RESET} Resource is gone: {human}");
if human.contains(CARVE_OUT_MARKER) {
let bin = bin_name();
eprintln!(
" {DIM}This usually means the bootstrap carve-out has already been \
used. For a second admin, run `{bin} bootstrap provision-request` from \
the new operator's host and have an existing admin run \
`{bin} bootstrap provision-integration` against this VTA.{RESET}"
);
} else {
eprintln!(
" {DIM}This resource was single-use or time-limited, and has been \
consumed or has expired. Retrying will not help — restart the \
operation to get a fresh one.{RESET}"
);
}
}
VtaError::Validation(msg) => {
eprintln!("{RED}\u{2717}{RESET} Invalid request: {msg}");
}
VtaError::RateLimited {
limited_by,
retry_after,
limiter,
url,
} => {
let (headline, hints) = rate_limit_message(
*limited_by,
*retry_after,
limiter.as_deref(),
url.as_deref(),
chrono::Utc::now(),
bin_name(),
);
eprintln!("{RED}\u{2717}{RESET} {headline}");
for hint in hints {
eprintln!(" {DIM}{hint}{RESET}");
}
}
VtaError::Network(e) => {
eprintln!("{RED}\u{2717}{RESET} Network error: {e}");
eprintln!(" {DIM}Is the VTA reachable? Check its URL with `pnm vta info`.{RESET}");
}
VtaError::Server { status, body } => {
eprintln!("{RED}\u{2717}{RESET} Server error (HTTP {status}): {body}");
eprintln!(
" {DIM}This is a VTA-side failure. Check server logs or contact the operator.{RESET}"
);
}
VtaError::UnsupportedTransport(msg) => {
eprintln!("{RED}\u{2717}{RESET} Unsupported transport: {msg}");
eprintln!(
" {DIM}This operation requires a specific transport (REST or DIDComm). \
Check which mode your CLI is in and whether the endpoint supports it.{RESET}"
);
}
VtaError::DidcommTransport(msg) => {
eprintln!("{RED}\u{2717}{RESET} DIDComm transport error: {msg}");
eprintln!(
" {DIM}Mediator or peer unreachable. Retry after checking mediator \
connectivity.{RESET}"
);
}
VtaError::DidcommRemote { code, comment } => {
eprintln!("{RED}\u{2717}{RESET} Remote error ({code}): {comment}");
}
VtaError::Protocol(msg) => {
eprintln!("{RED}\u{2717}{RESET} Protocol error: {msg}");
}
VtaError::LastServiceRefused => {
let bin = bin_name();
eprintln!(
"{RED}\u{2717}{RESET} Refused: would leave the VTA with no advertised services."
);
eprintln!(
" {DIM}At least one transport (REST or DIDComm) must remain advertised. \
Enable the other transport first via `{bin} services <kind> enable …`, \
then retry.{RESET}"
);
}
VtaError::ServiceNotPresent => {
let bin = bin_name();
eprintln!("{RED}\u{2717}{RESET} Service is not present.");
eprintln!(
" {DIM}The service kind isn't currently enabled. Use `{bin} services \
<kind> enable …` to bring it online before updating, disabling, or rolling \
it back.{RESET}"
);
}
VtaError::ServiceAlreadyEnabled => {
let bin = bin_name();
eprintln!("{RED}\u{2717}{RESET} Service is already enabled.");
eprintln!(
" {DIM}Use `{bin} services <kind> update …` to change its configuration, \
or `{bin} services <kind> disable` to remove it.{RESET}"
);
}
VtaError::MediatorHandshakeFailed { reason } => {
eprintln!("{RED}\u{2717}{RESET} Mediator handshake failed: {reason}");
eprintln!(
" {DIM}Confirm the mediator DID is correct and the mediator is reachable. \
The reason above is the specific cause from the handshake protocol.{RESET}"
);
}
VtaError::DrainTtlOutOfBounds {
min,
max,
requested,
} => {
eprintln!(
"{RED}\u{2717}{RESET} Drain TTL {requested}s is outside the allowed range \
[{min}s, {max}s]."
);
eprintln!(
" {DIM}Pick a value within those bounds. The minimum applies when the \
command is delivered over DIDComm transport (so the listener stays up long \
enough for the response).{RESET}"
);
}
VtaError::NoPriorMutation => {
let bin = bin_name();
eprintln!("{RED}\u{2717}{RESET} No prior mutation to roll back.");
eprintln!(
" {DIM}Use `{bin} services <kind> {{enable,update,disable}} …` directly \
instead of rollback.{RESET}"
);
}
other => eprintln!("{RED}\u{2717}{RESET} Error: {other}"),
}
return;
}
eprintln!("{RED}\u{2717}{RESET} Error: {err}");
let mut source = err.source();
while let Some(s) = source {
eprintln!(" {DIM}caused by: {s}{RESET}");
source = s.source();
}
}
fn rate_limit_message(
limited_by: vta_sdk::rate_limit::RateLimitSource,
retry_after: Option<chrono::DateTime<chrono::Utc>>,
limiter: Option<&str>,
url: Option<&str>,
now: chrono::DateTime<chrono::Utc>,
bin: &str,
) -> (String, Vec<String>) {
use vta_sdk::rate_limit::{self as rl, RateLimitSource};
let limiter = limiter.map(|l| format!(" ({l})")).unwrap_or_default();
let headline = format!(
"Rate limited by {}{limiter} — the request was refused, not failed.",
limited_by.label()
);
let mut hints = Vec::new();
hints.push(match retry_after {
Some(at) => {
let ms = (at - now).num_milliseconds().max(0);
let secs = (ms + 999) / 1000;
format!("Wait {secs}s before retrying.")
}
None => "No wait was given; wait a few seconds before retrying.".to_string(),
});
if let Some(url) = url {
hints.push(format!("Refused request: {url}"));
}
match limited_by {
RateLimitSource::Vta => {
hints.push(format!(
"To loosen it, in the VTA's `[server]` config: `{}` / `{}` for the auth and \
bootstrap endpoints, `{}` / `{}` for the VTA's own did.jsonl. Intervals are \
seconds per token, so lower is looser.",
rl::VTA_INTERVAL_KEY,
rl::VTA_BURST_KEY,
rl::VTA_DID_LOG_INTERVAL_KEY,
rl::VTA_DID_LOG_BURST_KEY,
));
hints.push(format!(
"At runtime: `{bin} {}`, or `{bin} {}`.",
rl::VTA_RUNTIME_FLAGS,
rl::VTA_DID_LOG_RUNTIME_FLAGS,
));
hints.push(format!(
"Behind a reverse proxy with `{} = false`, every client shares one bucket. \
See {}.",
rl::VTA_TRUST_XFF_KEY,
rl::VTA_DOCS,
));
}
RateLimitSource::Mediator => hints.push(format!(
"This is the DIDComm/TSP mediator, not the VTA. Its operator tunes {} — requests \
per second, so higher is looser.",
rl::MEDIATOR_KEYS,
)),
RateLimitSource::Upstream => {
hints.push(format!(
"The response carried no `{}` header, so nothing says who sent it: a reverse \
proxy or load balancer in front of the VTA, or a VTA older than that header.",
rl::SOURCE_HEADER,
));
hints.push(format!(
"Check the proxy / load balancer's limits and logs, or upgrade the VTA so its \
own refusals are labelled. See {}.",
rl::VTA_DOCS,
));
}
other => hints.push(rl::suggested_fix(other).to_string()),
}
(headline, hints)
}
const CARVE_OUT_MARKER: &str = "carve-out";
fn extract_human_message(body: &str) -> String {
serde_json::from_str::<serde_json::Value>(body)
.ok()
.and_then(|v| {
v.get("message")
.or_else(|| v.get("error"))
.and_then(|m| m.as_str())
.map(str::to_string)
})
.unwrap_or_else(|| body.to_string())
}
pub fn print_widget(widget: impl Widget, height: u16) {
let width = ratatui::crossterm::terminal::size().map_or(120, |(w, _)| w);
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
widget.render(area, &mut buf);
let mut out = String::new();
for y in 0..height {
let mut cur_fg = Color::Reset;
let mut cur_bg = Color::Reset;
let mut cur_mod = Modifier::empty();
for x in 0..width {
let cell = &buf[(x, y)];
if cell.diff_option == CellDiffOption::Skip {
continue;
}
if cell.fg != cur_fg || cell.bg != cur_bg || cell.modifier != cur_mod {
out.push_str("\x1b[0m");
push_ansi_fg(&mut out, cell.fg);
push_ansi_bg(&mut out, cell.bg);
push_ansi_mod(&mut out, cell.modifier);
cur_fg = cell.fg;
cur_bg = cell.bg;
cur_mod = cell.modifier;
}
out.push_str(cell.symbol());
}
out.push_str("\x1b[0m\n");
}
print!("{out}");
}
pub fn push_ansi_fg(out: &mut String, color: Color) {
use std::fmt::Write as _;
match color {
Color::Reset => {}
Color::Black => out.push_str("\x1b[30m"),
Color::Red => out.push_str("\x1b[31m"),
Color::Green => out.push_str("\x1b[32m"),
Color::Yellow => out.push_str("\x1b[33m"),
Color::Blue => out.push_str("\x1b[34m"),
Color::Magenta => out.push_str("\x1b[35m"),
Color::Cyan => out.push_str("\x1b[36m"),
Color::Gray => out.push_str("\x1b[37m"),
Color::DarkGray => out.push_str("\x1b[90m"),
Color::LightRed => out.push_str("\x1b[91m"),
Color::LightGreen => out.push_str("\x1b[92m"),
Color::LightYellow => out.push_str("\x1b[93m"),
Color::LightBlue => out.push_str("\x1b[94m"),
Color::LightMagenta => out.push_str("\x1b[95m"),
Color::LightCyan => out.push_str("\x1b[96m"),
Color::White => out.push_str("\x1b[97m"),
Color::Rgb(r, g, b) => {
let _ = write!(out, "\x1b[38;2;{r};{g};{b}m");
}
Color::Indexed(i) => {
let _ = write!(out, "\x1b[38;5;{i}m");
}
}
}
pub fn push_ansi_bg(out: &mut String, color: Color) {
use std::fmt::Write as _;
match color {
Color::Reset => {}
Color::Black => out.push_str("\x1b[40m"),
Color::Red => out.push_str("\x1b[41m"),
Color::Green => out.push_str("\x1b[42m"),
Color::Yellow => out.push_str("\x1b[43m"),
Color::Blue => out.push_str("\x1b[44m"),
Color::Magenta => out.push_str("\x1b[45m"),
Color::Cyan => out.push_str("\x1b[46m"),
Color::Gray => out.push_str("\x1b[47m"),
Color::DarkGray => out.push_str("\x1b[100m"),
Color::LightRed => out.push_str("\x1b[101m"),
Color::LightGreen => out.push_str("\x1b[102m"),
Color::LightYellow => out.push_str("\x1b[103m"),
Color::LightBlue => out.push_str("\x1b[104m"),
Color::LightMagenta => out.push_str("\x1b[105m"),
Color::LightCyan => out.push_str("\x1b[106m"),
Color::White => out.push_str("\x1b[107m"),
Color::Rgb(r, g, b) => {
let _ = write!(out, "\x1b[48;2;{r};{g};{b}m");
}
Color::Indexed(i) => {
let _ = write!(out, "\x1b[48;5;{i}m");
}
}
}
pub fn push_ansi_mod(out: &mut String, modifier: Modifier) {
if modifier.contains(Modifier::BOLD) {
out.push_str("\x1b[1m");
}
if modifier.contains(Modifier::DIM) {
out.push_str("\x1b[2m");
}
if modifier.contains(Modifier::ITALIC) {
out.push_str("\x1b[3m");
}
if modifier.contains(Modifier::UNDERLINED) {
out.push_str("\x1b[4m");
}
if modifier.contains(Modifier::REVERSED) {
out.push_str("\x1b[7m");
}
if modifier.contains(Modifier::CROSSED_OUT) {
out.push_str("\x1b[9m");
}
}
pub fn print_section(title: &str) {
let pad = 46usize.saturating_sub(title.len());
println!(
"\n{DIM}──{RESET} {BOLD}{title}{RESET} {DIM}{}{RESET}",
"─".repeat(pad)
);
}
#[cfg(test)]
mod rate_limit_render_tests {
use super::rate_limit_message;
use vta_sdk::rate_limit::RateLimitSource;
fn now() -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::parse_from_rfc3339("2026-09-16T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc)
}
#[test]
fn a_vta_refusal_names_the_vta_the_wait_and_every_knob() {
let (headline, hints) = rate_limit_message(
RateLimitSource::Vta,
Some(now() + chrono::Duration::milliseconds(3_200)),
Some("auth"),
Some("https://vta.example.com/auth/challenge"),
now(),
"pnm",
);
assert_eq!(
headline,
"Rate limited by the VTA (auth) — the request was refused, not failed."
);
let all = hints.join("\n");
assert_eq!(hints[0], "Wait 4s before retrying.", "rounded up");
for needle in [
"Refused request: https://vta.example.com/auth/challenge",
"`rate_limit_interval_secs` / `rate_limit_burst`",
"`did_log_rate_limit_interval_secs` / `did_log_rate_limit_burst`",
"lower is looser",
"`pnm config update --rate-limit-interval-secs <N> --rate-limit-burst <N>`",
"`pnm config update --did-log-rate-limit-interval-secs <N> --did-log-rate-limit-burst <N>`",
"`trust_xff = false`",
"docs/02-vta/rate-limiting.md",
] {
assert!(all.contains(needle), "missing {needle:?} in:\n{all}");
}
}
#[test]
fn an_unlabelled_refusal_points_at_the_proxy_and_uses_the_invoked_binary() {
let (headline, hints) =
rate_limit_message(RateLimitSource::Upstream, None, None, None, now(), "cnm");
assert!(headline.contains("unidentified service"), "{headline}");
let all = hints.join("\n");
assert!(all.contains("No wait was given"), "{all}");
assert!(all.contains("`x-rate-limit-source`"), "{all}");
assert!(all.contains("load balancer"), "{all}");
assert!(
!all.contains("config update"),
"an unattributed 429 must not send the operator to retune the VTA:\n{all}"
);
}
#[test]
fn a_mediator_refusal_names_the_mediator_limits_not_the_vta_config() {
let (headline, hints) = rate_limit_message(
RateLimitSource::Mediator,
Some(now() - chrono::Duration::seconds(5)),
None,
None,
now(),
"pnm",
);
assert!(headline.contains("the mediator"), "{headline}");
let all = hints.join("\n");
assert!(all.contains("Wait 0s"), "a stale hint is no wait: {all}");
assert!(all.contains("rate_limit_per_ip"), "{all}");
assert!(!all.contains("rate_limit_interval_secs"), "{all}");
}
#[test]
fn a_did_host_refusal_says_it_is_not_tunable_from_the_vta() {
let (_, hints) =
rate_limit_message(RateLimitSource::DidHost, None, None, None, now(), "pnm");
assert!(hints.join("\n").contains("not tunable from the VTA"));
}
}
#[cfg(test)]
mod tests {
use super::extract_human_message;
#[test]
fn prefers_message_field() {
let body = r#"{"error":"didcomm_already_enabled","message":"DIDComm is already enabled.","mediator_did":"did:peer:2.med"}"#;
assert_eq!(extract_human_message(body), "DIDComm is already enabled.");
}
#[test]
fn falls_back_to_error_field_when_no_message() {
let body = r#"{"error":"duplicate_key"}"#;
assert_eq!(extract_human_message(body), "duplicate_key");
}
#[test]
fn falls_back_to_raw_text_for_non_json() {
let body = "plain conflict text";
assert_eq!(extract_human_message(body), "plain conflict text");
}
#[test]
fn falls_back_to_raw_text_when_fields_missing() {
let body = r#"{"detail":"something"}"#;
assert_eq!(extract_human_message(body), body);
}
}