1use std::io::{self, Write};
2
3use anyhow::{Context, Result, anyhow};
4use clap::{Parser, Subcommand};
5
6use crate::commands::executable::ExecutableCommand;
7use crate::config::CliConfig;
8use crate::factories::storage::build_auths_context;
9use auths_crypto::AuthsErrorInfo;
10use auths_infra_http::resolve_verified_platform_context;
11use auths_sdk::domains::identity::registration::DEFAULT_REGISTRY_URL;
12use auths_sdk::keychain::KeyAlias;
13use auths_sdk::namespace_registry::NamespaceVerifierRegistry;
14use auths_sdk::ports::{Ecosystem, PackageName};
15use auths_sdk::signing::StorageSigner;
16use auths_sdk::storage_layout::layout;
17use auths_sdk::workflows::namespace::{
18 DelegateNamespaceCommand, TransferNamespaceCommand, initiate_namespace_claim,
19 parse_claim_response, parse_lookup_response, sign_namespace_delegate, sign_namespace_transfer,
20};
21use auths_verifier::CanonicalDid;
22
23#[derive(Parser, Debug, Clone)]
25#[command(
26 about = "Manage namespace claims in package ecosystems",
27 after_help = "Examples:
28 auths namespace claim --ecosystem npm --package-name @myname/package
29 # Claim a namespace in npm registry
30 auths namespace delegate --ecosystem crates.io --package-name mypackage \\
31 --recipient did:keri:ETarget
32 # Delegate authority to another identity
33 auths namespace transfer --ecosystem pypi --package-name mypackage \\
34 --owner did:keri:ENewOwner
35 # Transfer namespace to a new owner
36
37Related:
38 auths id — Manage identities
39 auths policy — Define capability policies
40 auths org — Manage organization memberships"
41)]
42pub struct NamespaceCommand {
43 #[clap(subcommand)]
44 pub subcommand: NamespaceSubcommand,
45}
46
47#[derive(Subcommand, Debug, Clone)]
49pub enum NamespaceSubcommand {
50 Claim {
56 #[arg(long)]
58 ecosystem: String,
59
60 #[arg(long)]
62 package_name: String,
63
64 #[arg(long)]
66 registry_url: Option<String>,
67
68 #[arg(long)]
70 key: Option<String>,
71 },
72
73 Delegate {
75 #[arg(long)]
77 ecosystem: String,
78
79 #[arg(long)]
81 package_name: String,
82
83 #[arg(long)]
85 delegate_did: String,
86
87 #[arg(long)]
89 registry_url: Option<String>,
90
91 #[arg(long)]
93 key: Option<String>,
94 },
95
96 Transfer {
98 #[arg(long)]
100 ecosystem: String,
101
102 #[arg(long)]
104 package_name: String,
105
106 #[arg(long)]
108 new_owner_did: String,
109
110 #[arg(long)]
112 registry_url: Option<String>,
113
114 #[arg(long)]
116 key: Option<String>,
117 },
118
119 Lookup {
121 #[arg(long)]
123 ecosystem: String,
124
125 #[arg(long)]
127 package_name: String,
128
129 #[arg(long)]
131 registry_url: Option<String>,
132 },
133}
134
135impl ExecutableCommand for NamespaceCommand {
136 #[allow(clippy::disallowed_methods)]
137 fn execute(&self, ctx: &CliConfig) -> Result<()> {
138 handle_namespace(self.clone(), ctx)
139 }
140}
141
142fn resolve_registry_url(registry_url: Option<String>) -> String {
143 registry_url.unwrap_or_else(|| DEFAULT_REGISTRY_URL.to_string())
144}
145
146fn load_identity_and_alias(
147 ctx: &CliConfig,
148 key: Option<String>,
149) -> Result<(
150 auths_verifier::types::IdentityDID,
151 KeyAlias,
152 auths_sdk::context::AuthsContext,
153)> {
154 let repo_path = layout::resolve_repo_path(ctx.repo_path.clone())?;
155 let auths_ctx = build_auths_context(
156 &repo_path,
157 &ctx.env_config,
158 Some(ctx.passphrase_provider.clone()),
159 )?;
160 let managed_identity = auths_ctx
161 .identity_storage
162 .load_identity()
163 .context("Failed to load identity. Run `auths init` first.")?;
164
165 let controller_did = managed_identity.controller_did;
166
167 let alias_str = key.unwrap_or_else(|| {
168 let prefix = controller_did
169 .as_str()
170 .strip_prefix("did:keri:")
171 .unwrap_or(controller_did.as_str());
172 format!(
173 "ns-{}",
174 prefix
175 .chars()
176 .filter(|c| c.is_alphanumeric())
177 .take(20)
178 .collect::<String>()
179 .to_lowercase()
180 )
181 });
182
183 let key_alias = KeyAlias::new_unchecked(alias_str);
184 Ok((controller_did, key_alias, auths_ctx))
185}
186
187fn post_signed_entry(registry_url: &str, body: serde_json::Value) -> Result<serde_json::Value> {
188 let url = format!("{}/v1/log/entries", registry_url.trim_end_matches('/'));
189
190 let client = reqwest::blocking::Client::new();
191 let response = client
192 .post(&url)
193 .json(&body)
194 .send()
195 .with_context(|| format!("Failed to POST to {url}"))?;
196
197 let status = response.status();
198 let response_text = response
199 .text()
200 .context("Failed to read registry response")?;
201
202 if !status.is_success() {
203 return Err(anyhow!(
204 "Registry returned HTTP {}: {}",
205 status,
206 response_text
207 ));
208 }
209
210 serde_json::from_str(&response_text).context("Failed to parse registry response")
211}
212
213#[allow(clippy::disallowed_methods)] pub fn handle_namespace(cmd: NamespaceCommand, ctx: &CliConfig) -> Result<()> {
216 match cmd.subcommand {
217 NamespaceSubcommand::Claim {
218 ecosystem,
219 package_name,
220 registry_url,
221 key,
222 } => {
223 let registry_url = resolve_registry_url(registry_url);
224 let (controller_did, key_alias, auths_ctx) = load_identity_and_alias(ctx, key)?;
225 let signer = StorageSigner::new(std::sync::Arc::clone(&auths_ctx.key_storage));
226 let passphrase_provider = ctx.passphrase_provider.clone();
227
228 let eco = Ecosystem::parse(&ecosystem).context("Failed to parse ecosystem")?;
229 let pkg = PackageName::parse(&package_name).context("Failed to parse package name")?;
230
231 #[allow(clippy::disallowed_methods)]
232 let canonical_did = CanonicalDid::new_unchecked(controller_did.as_str());
234
235 let rt_prefetch =
237 tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
238 let platform = rt_prefetch
239 .block_on(resolve_verified_platform_context(
240 ®istry_url,
241 controller_did.as_str(),
242 ))
243 .map_err(|e| anyhow!("{}", e))?;
244
245 let registry = NamespaceVerifierRegistry::with_defaults();
246 let verifier = registry
247 .require(eco)
248 .context("No verifier available for this ecosystem")?;
249
250 println!("Verifying ownership of {}/{}...\n", eco, package_name);
251
252 let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
253
254 let mut session = rt
255 .block_on(initiate_namespace_claim(
256 chrono::Utc::now(),
257 verifier.as_ref(),
258 eco,
259 pkg,
260 canonical_did,
261 platform,
262 ))
263 .context("Failed to initiate namespace verification")?;
264
265 println!(" {}\n", session.challenge.instructions);
266
267 let max_retries = 3;
270 let mut result = None;
271
272 for attempt in 0..max_retries {
273 match rt.block_on(session.complete_ref(
274 chrono::Utc::now(),
275 verifier.as_ref(),
276 &signer,
277 passphrase_provider.as_ref(),
278 &key_alias,
279 )) {
280 Ok(r) => {
281 result = Some(r);
282 break;
283 }
284 Err(auths_sdk::workflows::namespace::NamespaceError::VerificationFailed(
285 ref verify_err,
286 )) => {
287 use auths_sdk::ports::NamespaceVerifyError;
288 match verify_err {
289 NamespaceVerifyError::OwnershipNotConfirmed { ecosystem, .. }
290 if attempt + 1 < max_retries =>
291 {
292 match *ecosystem {
294 Ecosystem::Pypi if session.platform.pypi_username.is_none() => {
295 eprintln!(
296 "\nAutomatic verification didn't match. \
297 Let's try your PyPI username."
298 );
299 print!("What's your PyPI username? ");
300 io::stdout().flush().ok();
301 let mut username = String::new();
302 let _ = io::stdin().read_line(&mut username);
303 let username = username.trim().to_string();
304 if !username.is_empty() {
305 session.platform.pypi_username = Some(username);
306 eprintln!("Retrying with PyPI username...\n");
307 }
308 continue;
309 }
310 Ecosystem::Npm if session.platform.npm_username.is_none() => {
311 eprintln!(
312 "\nAutomatic verification didn't match. \
313 Let's try your npm username."
314 );
315 print!("What's your npm username? ");
316 io::stdout().flush().ok();
317 let mut username = String::new();
318 let _ = io::stdin().read_line(&mut username);
319 let username = username.trim().to_string();
320 if !username.is_empty() {
321 session.platform.npm_username = Some(username);
322 eprintln!("Retrying with npm username...\n");
323 }
324 continue;
325 }
326 _ => {
327 eprintln!(
328 "\nVerification not confirmed. Did you complete the step above?"
329 );
330 print!("Press Enter to retry, or Ctrl+C to cancel...");
331 io::stdout().flush().ok();
332 let _ = io::stdin().read_line(&mut String::new());
333 continue;
334 }
335 }
336 }
337 _ => {
338 eprintln!("\n✗ Verification failed [{}]", verify_err.error_code());
339 eprintln!(" {verify_err}");
340 if let Some(hint) = verify_err.suggestion() {
341 eprintln!("\n Hint: {hint}");
342 }
343 return Err(anyhow!("{}", verify_err));
344 }
345 }
346 }
347 Err(e) => return Err(e).context("Namespace verification failed"),
348 }
349 }
350
351 let result = result
352 .ok_or_else(|| anyhow!("Verification failed after {max_retries} attempts"))?;
353
354 println!("\nChecking... ✓ Verified!\n");
355 println!("Claiming namespace {}/{}...", eco, package_name);
356
357 let response = post_signed_entry(®istry_url, result.signed_entry.to_request_body())?;
358
359 let claim_result = parse_claim_response(
360 eco.as_str(),
361 &package_name,
362 controller_did.as_str(),
363 &response,
364 );
365
366 println!("\n✓ Namespace {}/{} claimed", eco, package_name);
367 println!(" Log sequence: {}", claim_result.log_sequence);
368
369 Ok(())
370 }
371
372 NamespaceSubcommand::Delegate {
373 ecosystem,
374 package_name,
375 delegate_did,
376 registry_url,
377 key,
378 } => {
379 let registry_url = resolve_registry_url(registry_url);
380 let (controller_did, key_alias, auths_ctx) = load_identity_and_alias(ctx, key)?;
381 let signer = StorageSigner::new(std::sync::Arc::clone(&auths_ctx.key_storage));
382 let passphrase_provider = ctx.passphrase_provider.clone();
383
384 println!(
385 "Delegating namespace {}/{} to {}...",
386 ecosystem, package_name, delegate_did
387 );
388
389 let sdk_cmd = DelegateNamespaceCommand {
390 ecosystem: ecosystem.clone(),
391 package_name: package_name.clone(),
392 delegate_did: delegate_did.clone(),
393 registry_url: registry_url.clone(),
394 };
395
396 let signed = sign_namespace_delegate(
397 &sdk_cmd,
398 &controller_did,
399 &signer,
400 passphrase_provider.as_ref(),
401 &key_alias,
402 )
403 .context("Failed to sign namespace delegation")?;
404
405 post_signed_entry(®istry_url, signed.to_request_body())?;
406
407 println!("\nNamespace delegation successful!");
408 println!(" Ecosystem: {}", ecosystem);
409 println!(" Package: {}", package_name);
410 println!(" Delegate: {}", delegate_did);
411
412 Ok(())
413 }
414
415 NamespaceSubcommand::Transfer {
416 ecosystem,
417 package_name,
418 new_owner_did,
419 registry_url,
420 key,
421 } => {
422 let registry_url = resolve_registry_url(registry_url);
423 let (controller_did, key_alias, auths_ctx) = load_identity_and_alias(ctx, key)?;
424 let signer = StorageSigner::new(std::sync::Arc::clone(&auths_ctx.key_storage));
425 let passphrase_provider = ctx.passphrase_provider.clone();
426
427 println!(
428 "Transferring namespace {}/{} to {}...",
429 ecosystem, package_name, new_owner_did
430 );
431
432 let sdk_cmd = TransferNamespaceCommand {
433 ecosystem: ecosystem.clone(),
434 package_name: package_name.clone(),
435 new_owner_did: new_owner_did.clone(),
436 registry_url: registry_url.clone(),
437 };
438
439 let signed = sign_namespace_transfer(
440 &sdk_cmd,
441 &controller_did,
442 &signer,
443 passphrase_provider.as_ref(),
444 &key_alias,
445 )
446 .context("Failed to sign namespace transfer")?;
447
448 post_signed_entry(®istry_url, signed.to_request_body())?;
449
450 println!("\nNamespace transfer successful!");
451 println!(" Ecosystem: {}", ecosystem);
452 println!(" Package: {}", package_name);
453 println!(" New Owner: {}", new_owner_did);
454
455 Ok(())
456 }
457
458 NamespaceSubcommand::Lookup {
459 ecosystem,
460 package_name,
461 registry_url,
462 } => {
463 let registry_url = resolve_registry_url(registry_url);
464
465 println!("Looking up namespace {}/{}...", ecosystem, package_name);
466
467 let url = format!(
468 "{}/v1/namespaces/{}/{}",
469 registry_url.trim_end_matches('/'),
470 ecosystem,
471 package_name
472 );
473
474 let client = reqwest::blocking::Client::new();
475 let response = client
476 .get(&url)
477 .send()
478 .with_context(|| format!("Failed to GET {url}"))?;
479
480 if response.status() == reqwest::StatusCode::NOT_FOUND {
481 println!("\nNamespace {}/{} is not claimed.", ecosystem, package_name);
482 return Ok(());
483 }
484
485 if !response.status().is_success() {
486 let status = response.status();
487 let body = response.text().unwrap_or_default();
488 return Err(anyhow!("Registry returned HTTP {}: {}", status, body));
489 }
490
491 let body: serde_json::Value = response
492 .json()
493 .context("Failed to parse registry response")?;
494
495 let info = parse_lookup_response(&ecosystem, &package_name, &body);
496
497 println!("\nNamespace: {}/{}", info.ecosystem, info.package_name);
498 println!(" Owner: {}", info.owner_did);
499 if info.delegates.is_empty() {
500 println!(" Delegates: (none)");
501 } else {
502 println!(" Delegates:");
503 for d in &info.delegates {
504 println!(" - {}", d);
505 }
506 }
507
508 Ok(())
509 }
510 }
511}