auths_cli/commands/artifact/
publish.rs1use std::path::Path;
2use std::time::Duration;
3
4use anyhow::{Context, Result, bail};
5use serde::{Deserialize, Serialize};
6
7use crate::ux::format::{JsonResponse, Output, is_json_mode};
8
9#[derive(Serialize)]
10struct PublishJsonResponse {
11 attestation_rid: String,
12 registry: String,
13 package_name: Option<String>,
14 signer_did: String,
15}
16
17#[derive(Deserialize)]
18struct ArtifactPublishResponse {
19 attestation_rid: String,
20 package_name: Option<String>,
21 signer_did: String,
22}
23
24pub fn handle_publish(signature_path: &Path, package: Option<&str>, registry: &str) -> Result<()> {
36 let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
37 rt.block_on(handle_publish_async(signature_path, package, registry))
38}
39
40fn validate_package_identifier(package: &str) -> Result<String> {
41 let trimmed = package.trim();
42 if trimmed.is_empty() {
43 bail!("Package identifier must not be empty.");
44 }
45 if !trimmed.contains(':') {
46 bail!(
47 "Package identifier must contain an ecosystem prefix (e.g., npm:react@18.3.0), got: {}",
48 trimmed
49 );
50 }
51 if trimmed.chars().any(|c| c.is_ascii_control() || c == ' ') {
52 bail!(
53 "Package identifier must not contain whitespace or control characters, got: {}",
54 trimmed
55 );
56 }
57 Ok(trimmed.to_lowercase())
58}
59
60async fn handle_publish_async(
61 signature_path: &Path,
62 package: Option<&str>,
63 registry: &str,
64) -> Result<()> {
65 if !signature_path.exists() {
66 bail!(
67 "Signature file not found: {:?}\nRun `auths artifact sign` first to create a signature file.",
68 signature_path
69 );
70 }
71
72 let sig_contents = std::fs::read_to_string(signature_path)
73 .with_context(|| format!("Failed to read signature file: {:?}", signature_path))?;
74
75 let attestation: serde_json::Value =
76 serde_json::from_str(&sig_contents).with_context(|| {
77 format!(
78 "Failed to parse signature file as JSON: {:?}",
79 signature_path
80 )
81 })?;
82
83 let package_name = if let Some(pkg) = package {
86 Some(validate_package_identifier(pkg)?)
87 } else {
88 let has_name = attestation
89 .get("payload")
90 .and_then(|p| p.get("name"))
91 .and_then(|n| n.as_str())
92 .is_some_and(|s| !s.is_empty());
93 if !has_name && !is_json_mode() {
94 eprintln!(
95 "Warning: No --package specified and no name in attestation payload. \
96 This artifact won't be discoverable by package query."
97 );
98 }
99 None
100 };
101
102 let registry_url = registry.trim_end_matches('/');
103 let response = transmit_publish(registry_url, &attestation, package_name.as_deref()).await?;
104 let status = response.status();
105
106 match status.as_u16() {
107 201 => {
108 let body: ArtifactPublishResponse = response
109 .json()
110 .await
111 .context("Failed to parse publish response")?;
112
113 if is_json_mode() {
114 let json_resp = JsonResponse::success(
115 "artifact publish",
116 PublishJsonResponse {
117 attestation_rid: body.attestation_rid.clone(),
118 registry: registry_url.to_string(),
119 package_name: body.package_name.clone(),
120 signer_did: body.signer_did.clone(),
121 },
122 );
123 json_resp.print()?;
124 } else {
125 let out = Output::stdout();
126 if let Some(ref pkg) = body.package_name {
127 println!("Anchoring signature for {}...", out.info(pkg));
128 }
129 println!(
130 "{} Cryptographic attestation anchored at {}",
131 out.success("Success!"),
132 out.bold(registry_url)
133 );
134 println!("Attestation RID: {}", out.info(&body.attestation_rid));
135 println!();
136 if let Some(ref pkg) = body.package_name {
137 println!(
138 "View your trust graph online: {}/registry?q={}",
139 registry_url, pkg
140 );
141 }
142 }
143 }
144 409 => {
145 bail!("Artifact attestation already published (duplicate RID).");
146 }
147 422 => {
148 let body = response.text().await.unwrap_or_default();
149 bail!("Signature verification failed at registry: {}", body);
150 }
151 _ => {
152 let body = response.text().await.unwrap_or_default();
153 bail!("Registry error ({}): {}", status, body);
154 }
155 }
156
157 Ok(())
158}
159
160async fn transmit_publish(
161 registry: &str,
162 attestation: &serde_json::Value,
163 package_name: Option<&str>,
164) -> Result<reqwest::Response> {
165 let client = reqwest::Client::builder()
166 .connect_timeout(Duration::from_secs(30))
167 .timeout(Duration::from_secs(60))
168 .build()
169 .context("Failed to create HTTP client")?;
170
171 let endpoint = format!("{}/v1/artifacts/publish", registry);
172 let mut body = serde_json::json!({ "attestation": attestation });
173 if let Some(name) = package_name {
174 body["package_name"] = serde_json::Value::String(name.to_string());
175 }
176 client
177 .post(&endpoint)
178 .json(&body)
179 .send()
180 .await
181 .context("Failed to connect to registry server")
182}