auths_cli/commands/id/
claim.rs1use std::path::Path;
2use std::sync::Arc;
3
4use anyhow::{Context, Result};
5use clap::{Parser, Subcommand};
6use serde::{Deserialize, Serialize};
7
8use auths_core::signing::PassphraseProvider;
9use auths_id::storage::identity::IdentityStorage;
10use auths_storage::git::RegistryIdentityStorage;
11
12use crate::services::providers::github::GitHubProvider;
13use crate::services::providers::{ClaimContext, PlatformClaimProvider};
14use crate::ux::format::{JsonResponse, Output, is_json_mode};
15
16use super::register::DEFAULT_REGISTRY_URL;
17
18#[derive(Parser, Debug, Clone)]
19#[command(about = "Add a platform claim to an already-registered identity.")]
20pub struct ClaimCommand {
21 #[command(subcommand)]
22 pub platform: ClaimPlatform,
23
24 #[arg(long, default_value = DEFAULT_REGISTRY_URL)]
25 pub registry: String,
26}
27
28#[derive(Subcommand, Debug, Clone)]
29pub enum ClaimPlatform {
30 Github,
32}
33
34#[derive(Serialize)]
35struct ClaimJsonResponse {
36 platform: String,
37 namespace: String,
38 did: String,
39}
40
41#[derive(Deserialize)]
42struct ServerClaimResponse {
43 platform: String,
44 namespace: String,
45 did: String,
46}
47
48pub fn handle_claim(
49 cmd: &ClaimCommand,
50 repo_path: &Path,
51 passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
52 http_client: &reqwest::Client,
53) -> Result<()> {
54 let out = Output::stdout();
55
56 let identity_storage = RegistryIdentityStorage::new(repo_path.to_path_buf());
57 let identity = identity_storage
58 .load_identity()
59 .context("Failed to load identity. Run `auths init` first.")?;
60
61 let controller_did = &identity.controller_did;
62
63 let provider: Box<dyn PlatformClaimProvider> = match cmd.platform {
64 ClaimPlatform::Github => Box::new(GitHubProvider),
65 };
66
67 let ctx = ClaimContext {
68 out: &out,
69 controller_did: controller_did.as_str(),
70 key_alias: "main",
71 passphrase_provider: passphrase_provider.as_ref(),
72 http_client,
73 };
74
75 let auth = provider.authenticate_and_publish(&ctx)?;
76
77 out.print_info("Submitting claim to registry...");
78
79 let rt = tokio::runtime::Runtime::new().context("failed to create async runtime")?;
80 let resp = rt.block_on(submit_claim(
81 http_client,
82 &cmd.registry,
83 controller_did.as_str(),
84 &auth.proof_url,
85 ))?;
86
87 if is_json_mode() {
88 let response = JsonResponse::success(
89 "id claim",
90 ClaimJsonResponse {
91 platform: resp.platform.clone(),
92 namespace: resp.namespace.clone(),
93 did: resp.did.clone(),
94 },
95 );
96 response.print()?;
97 } else {
98 out.print_success(&format!(
99 "Platform claim indexed: {} @{} -> {}",
100 resp.platform, resp.namespace, resp.did
101 ));
102 }
103
104 Ok(())
105}
106
107async fn submit_claim(
108 client: &reqwest::Client,
109 registry_url: &str,
110 did: &str,
111 proof_url: &str,
112) -> Result<ServerClaimResponse> {
113 let url = format!(
114 "{}/v1/identities/{}/claims",
115 registry_url.trim_end_matches('/'),
116 did
117 );
118
119 let resp = client
120 .post(&url)
121 .header("Content-Type", "application/json")
122 .json(&serde_json::json!({ "proof_url": proof_url }))
123 .send()
124 .await
125 .context("failed to submit claim to registry")?;
126
127 let status = resp.status();
128
129 if status.as_u16() == 404 {
130 anyhow::bail!("Identity not found at registry. Run `auths id register` first.");
131 }
132
133 if !status.is_success() {
134 let body = resp.text().await.unwrap_or_default();
135 anyhow::bail!("Registry returned {}: {}", status, body);
136 }
137
138 resp.json::<ServerClaimResponse>()
139 .await
140 .context("failed to parse registry response")
141}