vta_cli_common/commands/
agent_names.rs1use vta_sdk::prelude::*;
16
17use crate::display::shorten_did;
18use crate::render::{BOLD, CYAN, DIM, GREEN, RESET, YELLOW, is_json_output, print_json};
19
20fn qualified(did: &str, name: &str) -> String {
27 match domain_of(did) {
28 Some(domain) => format!("{domain}/@{name}"),
29 None => format!("@{name}"),
30 }
31}
32
33fn domain_of(did: &str) -> Option<String> {
38 let mut parts = did.split(':');
39 if parts.next()? != "did" {
40 return None;
41 }
42 let method = parts.next()?;
43 if method != "webvh" && method != "web" {
44 return None;
45 }
46 let _scid = parts.next()?;
47 let host = parts.next()?;
48 Some(host.replace("%3A", ":").replace("%3a", ":"))
49}
50
51pub async fn cmd_agent_name_set(
52 client: &VtaClient,
53 did: &str,
54 name: &str,
55) -> Result<(), Box<dyn std::error::Error>> {
56 let result = client.set_agent_name(did, name).await?;
57 if is_json_output() {
58 print_json(&result)?;
59 return Ok(());
60 }
61 println!("{GREEN}\u{2713}{RESET} Agent name bound.");
62 println!(
63 " {CYAN}Name:{RESET} {BOLD}{}{RESET}",
64 qualified(did, name)
65 );
66 println!(" {CYAN}DID:{RESET} {did}");
67 println!(
68 " {DIM}The DID document now claims this name; the hosting server \
69 serves it as a redirect.{RESET}"
70 );
71 Ok(())
72}
73
74pub async fn cmd_agent_name_remove(
75 client: &VtaClient,
76 did: &str,
77 name: &str,
78) -> Result<(), Box<dyn std::error::Error>> {
79 let result = client.remove_agent_name(did, name).await?;
80 if is_json_output() {
81 print_json(&result)?;
82 return Ok(());
83 }
84 println!(
85 "{GREEN}\u{2713}{RESET} Agent name released: {}",
86 qualified(did, name)
87 );
88 println!(
89 " {DIM}The claim is gone from the DID document and the name is free \
90 for anyone to claim. Use `disable` instead to keep it reserved.{RESET}"
91 );
92 Ok(())
93}
94
95pub async fn cmd_agent_name_disable(
96 client: &VtaClient,
97 did: &str,
98 name: &str,
99) -> Result<(), Box<dyn std::error::Error>> {
100 let result = client.disable_agent_name(did, name).await?;
101 if is_json_output() {
102 print_json(&result)?;
103 return Ok(());
104 }
105 println!(
106 "{GREEN}\u{2713}{RESET} Agent name parked: {}",
107 qualified(did, name)
108 );
109 println!(
110 " {DIM}It no longer resolves, but stays reserved to this DID. \
111 Re-enable with `agent-names enable`.{RESET}"
112 );
113 Ok(())
114}
115
116pub async fn cmd_agent_name_enable(
117 client: &VtaClient,
118 did: &str,
119 name: &str,
120) -> Result<(), Box<dyn std::error::Error>> {
121 let result = client.enable_agent_name(did, name).await?;
122 if is_json_output() {
123 print_json(&result)?;
124 return Ok(());
125 }
126 println!(
127 "{GREEN}\u{2713}{RESET} Agent name back in service: {}",
128 qualified(did, name)
129 );
130 Ok(())
131}
132
133pub async fn cmd_agent_name_list(
134 client: &VtaClient,
135 did: &str,
136) -> Result<(), Box<dyn std::error::Error>> {
137 let result = client.list_agent_names(did).await?;
138 if is_json_output() {
139 print_json(&result)?;
140 return Ok(());
141 }
142
143 if result.names.is_empty() {
144 println!("No agent names bound to {}.", shorten_did(did));
145 println!(" {DIM}Bind one with `agent-names set --did <did> --name <name>`.{RESET}");
146 return Ok(());
147 }
148
149 println!();
150 println!("{BOLD}Agent names for {}{RESET}", shorten_did(did));
151 println!();
152 for entry in &result.names {
153 let state = if entry.enabled {
157 format!("{GREEN}resolves{RESET}")
158 } else {
159 format!("{YELLOW}parked{RESET}")
160 };
161 println!(" {BOLD}{}{RESET} {state}", qualified(did, &entry.name));
162 }
163 println!();
164 Ok(())
165}
166
167pub async fn cmd_agent_name_check(
168 client: &VtaClient,
169 did: &str,
170 name: &str,
171) -> Result<(), Box<dyn std::error::Error>> {
172 let result = client.check_agent_name(did, name).await?;
173 if is_json_output() {
174 print_json(&result)?;
175 return Ok(());
176 }
177
178 let full = format!("{}/@{}", result.domain, result.name);
179 if result.reserved {
180 println!("{YELLOW}\u{2717}{RESET} {full} is reserved.");
181 println!(
182 " {DIM}Reserved names (admin, api, support, …) are withheld to \
183 make impersonation harder.{RESET}"
184 );
185 } else if result.available {
186 println!("{GREEN}\u{2713}{RESET} {full} is available.");
187 println!(
188 " {DIM}Advisory only — it can be taken before you bind it, so \
189 `set` reports its own conflict.{RESET}"
190 );
191 } else {
192 println!("{YELLOW}\u{2717}{RESET} {full} is taken.");
193 }
194 Ok(())
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn domain_comes_from_the_did_not_the_operator() {
203 assert_eq!(
204 domain_of("did:webvh:QmScid:webvh.storm.ws:glenn-vta").as_deref(),
205 Some("webvh.storm.ws")
206 );
207 assert_eq!(
208 domain_of("did:web:QmScid:example.com").as_deref(),
209 Some("example.com")
210 );
211 }
212
213 #[test]
214 fn a_ported_host_is_percent_decoded() {
215 assert_eq!(
218 domain_of("did:webvh:QmScid:localhost%3A8534").as_deref(),
219 Some("localhost:8534")
220 );
221 }
222
223 #[test]
224 fn a_non_web_did_has_no_domain() {
225 assert!(domain_of("did:key:z6MkfrQjWz").is_none());
226 assert!(domain_of("not-a-did").is_none());
227 }
228
229 #[test]
230 fn qualified_falls_back_to_the_bare_local_part() {
231 assert_eq!(qualified("did:key:z6MkfrQjWz", "ops"), "@ops");
233 assert_eq!(
234 qualified("did:webvh:QmScid:webvh.storm.ws", "ops"),
235 "webvh.storm.ws/@ops"
236 );
237 }
238}