Skip to main content

auths_cli/commands/id/
claim.rs

1use 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    /// Link your GitHub account to your identity.
31    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}