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