auths_cli/commands/id/
claim.rs1use std::path::Path;
2use std::sync::Arc;
3
4use anyhow::{Context, Result};
5use auths_infra_http::{
6 HttpGistPublisher, HttpGitHubOAuthProvider, HttpNpmAuthProvider, HttpRegistryClaimClient,
7};
8use auths_sdk::core_config::EnvironmentConfig;
9use auths_sdk::signing::PassphraseProvider;
10use auths_sdk::workflows::platform::{
11 GitHubClaimConfig, NpmClaimConfig, PypiClaimConfig, claim_github_identity, claim_npm_identity,
12 claim_pypi_identity,
13};
14use clap::{Parser, Subcommand};
15use console::style;
16
17use crate::factories::storage::build_auths_context;
18use crate::ux::format::{JsonResponse, is_json_mode};
19
20use super::register::DEFAULT_REGISTRY_URL;
21
22const DEFAULT_GITHUB_CLIENT_ID: &str = "Ov23lio2CiTHBjM2uIL4";
23
24#[allow(clippy::disallowed_methods)]
25fn github_client_id() -> String {
26 std::env::var("AUTHS_GITHUB_CLIENT_ID").unwrap_or_else(|_| DEFAULT_GITHUB_CLIENT_ID.to_string())
27}
28
29#[derive(Parser, Debug, Clone)]
30#[command(about = "Add a platform claim to an already-registered identity.")]
31pub struct ClaimCommand {
32 #[command(subcommand)]
33 pub platform: ClaimPlatform,
34}
35
36#[derive(Subcommand, Debug, Clone)]
37pub enum ClaimPlatform {
38 Github {
40 #[arg(long, env = "AUTHS_REGISTRY_URL", default_value = DEFAULT_REGISTRY_URL)]
42 registry: String,
43 },
44 Npm {
46 #[arg(long, env = "AUTHS_REGISTRY_URL", default_value = DEFAULT_REGISTRY_URL)]
48 registry: String,
49 },
50 Pypi {
52 #[arg(long, env = "AUTHS_REGISTRY_URL", default_value = DEFAULT_REGISTRY_URL)]
54 registry: String,
55 },
56}
57
58pub fn handle_claim(
59 cmd: &ClaimCommand,
60 repo_path: &Path,
61 passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
62 env_config: &EnvironmentConfig,
63 now: chrono::DateTime<chrono::Utc>,
64) -> Result<()> {
65 let registry_url = match &cmd.platform {
66 ClaimPlatform::Github { registry }
67 | ClaimPlatform::Npm { registry }
68 | ClaimPlatform::Pypi { registry } => registry.clone(),
69 };
70
71 let ctx = build_auths_context(repo_path, env_config, Some(passphrase_provider))
72 .context("Failed to build auths context")?;
73
74 let registry_client = HttpRegistryClaimClient::new();
75
76 match &cmd.platform {
77 ClaimPlatform::Github { .. } => {
78 let oauth = HttpGitHubOAuthProvider::new();
79 let publisher = HttpGistPublisher::new();
80
81 let config = GitHubClaimConfig {
82 client_id: github_client_id(),
83 registry_url,
84 scopes: "read:user gist".to_string(),
85 };
86
87 let on_device_code = |code: &auths_sdk::ports::platform::DeviceCodeResponse| {
88 println!();
89 println!(" Copy this code: {}", style(&code.user_code).bold().cyan());
90 println!(" At: {}", style(&code.verification_uri).cyan());
91 println!();
92 println!(
93 " {}",
94 style("Press 'enter' to open GitHub after copying the code above").blue()
95 );
96 let _ = std::io::stdin().read_line(&mut String::new());
97 println!();
98 if let Err(e) = open::that(&code.verification_uri) {
99 println!(
100 " {}",
101 style(format!("Could not open browser: {e}")).yellow()
102 );
103 println!(" Please open the URL above manually.");
104 } else {
105 println!(" Browser opened — waiting for authorization...");
106 }
107 };
108
109 let rt = tokio::runtime::Runtime::new().context("failed to create async runtime")?;
110 let response = rt
111 .block_on(claim_github_identity(
112 &oauth,
113 &publisher,
114 ®istry_client,
115 &ctx,
116 config,
117 now,
118 &on_device_code,
119 ))
120 .map_err(anyhow::Error::from)?;
121
122 print_response(&response.message)?;
123 }
124
125 ClaimPlatform::Npm { .. } => {
126 println!();
127 println!(" {}", style("npm platform claim").bold());
128 println!();
129 println!(
130 " Create a read-only access token at:\n {}",
131 style("https://www.npmjs.com/settings/~/tokens").cyan()
132 );
133 println!();
134
135 let npm_token = rpassword::prompt_password(" Enter your npm access token: ")
136 .context("Failed to read npm token")?;
137
138 if npm_token.trim().is_empty() {
139 return Err(anyhow::anyhow!("npm access token cannot be empty"));
140 }
141
142 println!(" Verifying token...");
143
144 let npm_provider = HttpNpmAuthProvider::new();
145 let rt = tokio::runtime::Runtime::new().context("failed to create async runtime")?;
146
147 let profile = rt
148 .block_on(npm_provider.verify_token(npm_token.trim()))
149 .map_err(anyhow::Error::from)?;
150
151 println!(
152 " {} Authenticated as {}",
153 style("✓").green(),
154 style(&profile.login).bold()
155 );
156
157 let config = NpmClaimConfig { registry_url };
158
159 let response = rt
160 .block_on(claim_npm_identity(
161 &profile.login,
162 npm_token.trim(),
163 ®istry_client,
164 &ctx,
165 config,
166 now,
167 ))
168 .map_err(anyhow::Error::from)?;
169
170 print_response(&response.message)?;
171 }
172
173 ClaimPlatform::Pypi { .. } => {
174 println!();
175 println!(" {}", style("PyPI platform claim").bold());
176 println!();
177 println!(
178 " Enter your PyPI username (visible at {}):",
179 style("https://pypi.org/account/").cyan()
180 );
181 println!();
182 println!(
183 " {}",
184 style("Note: ownership is verified when you claim a package namespace,").dim()
185 );
186 println!(
187 " {}",
188 style("not at this step. The PyPI JSON API confirms you are a maintainer.").dim()
189 );
190 println!();
191
192 print!(" PyPI username: ");
193 std::io::Write::flush(&mut std::io::stdout()).context("flush")?;
194 let mut pypi_username = String::new();
195 std::io::stdin()
196 .read_line(&mut pypi_username)
197 .context("Failed to read PyPI username")?;
198
199 let trimmed = pypi_username.trim();
200 if trimmed.is_empty() {
201 return Err(anyhow::anyhow!("PyPI username cannot be empty"));
202 }
203
204 println!(" Claiming PyPI identity as {}...", style(trimmed).bold());
205
206 let config = PypiClaimConfig { registry_url };
207
208 let rt = tokio::runtime::Runtime::new().context("failed to create async runtime")?;
209 let response = rt
210 .block_on(claim_pypi_identity(
211 trimmed,
212 ®istry_client,
213 &ctx,
214 config,
215 now,
216 ))
217 .map_err(anyhow::Error::from)?;
218
219 print_response(&response.message)?;
220 }
221 }
222
223 Ok(())
224}
225
226fn print_response(message: &str) -> Result<()> {
227 if is_json_mode() {
228 let json_resp = JsonResponse::success("id claim", message);
229 json_resp.print()?;
230 } else {
231 println!(" {}", style(message).green());
232 }
233 Ok(())
234}