use std::path::Path;
use std::time::Duration;
use anyhow::{Context, Result, bail};
use auths_infra_http::HttpRegistryClient;
use auths_sdk::workflows::artifact::{
ArtifactPublishConfig, ArtifactPublishError, publish_artifact,
};
use serde::Serialize;
use crate::ux::format::{JsonResponse, Output, is_json_mode};
#[derive(Serialize)]
struct PublishJsonResponse {
attestation_rid: String,
registry: String,
package_name: Option<String>,
signer_did: String,
}
pub fn handle_publish(signature_path: &Path, package: Option<&str>, registry: &str) -> Result<()> {
let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
rt.block_on(handle_publish_async(signature_path, package, registry))
}
fn validate_package_identifier(package: &str) -> Result<String> {
let trimmed = package.trim();
if trimmed.is_empty() {
bail!("Package identifier must not be empty.");
}
if !trimmed.contains(':') {
bail!(
"Package identifier must contain an ecosystem prefix (e.g., npm:react@18.3.0), got: {}",
trimmed
);
}
if trimmed.chars().any(|c| c.is_ascii_control() || c == ' ') {
bail!(
"Package identifier must not contain whitespace or control characters, got: {}",
trimmed
);
}
Ok(trimmed.to_lowercase())
}
async fn handle_publish_async(
signature_path: &Path,
package: Option<&str>,
registry: &str,
) -> Result<()> {
if !signature_path.exists() {
bail!(
"Signature file not found: {:?}\nRun `auths artifact sign` first to create a signature file.",
signature_path
);
}
let sig_contents = std::fs::read_to_string(signature_path)
.with_context(|| format!("Failed to read signature file: {:?}", signature_path))?;
let attestation: serde_json::Value =
serde_json::from_str(&sig_contents).with_context(|| {
format!(
"Failed to parse signature file as JSON: {:?}",
signature_path
)
})?;
let package_name = if let Some(pkg) = package {
Some(validate_package_identifier(pkg)?)
} else {
let has_name = attestation
.get("payload")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.is_some_and(|s| !s.is_empty());
if !has_name && !is_json_mode() {
eprintln!(
"Warning: No --package specified and no name in attestation payload. \
This artifact won't be discoverable by package query."
);
}
None
};
let registry_url = registry.trim_end_matches('/').to_string();
let registry_client =
HttpRegistryClient::new_with_timeouts(Duration::from_secs(30), Duration::from_secs(60));
let config = ArtifactPublishConfig {
attestation,
package_name,
registry_url: registry_url.clone(),
};
let body = publish_artifact(&config, ®istry_client)
.await
.map_err(|e| match e {
ArtifactPublishError::DuplicateAttestation => {
anyhow::anyhow!("Artifact attestation already published (duplicate RID).")
}
ArtifactPublishError::VerificationFailed(msg) => {
anyhow::anyhow!("Signature verification failed at registry: {}", msg)
}
ArtifactPublishError::RegistryError { status, body } => {
anyhow::anyhow!("Registry error ({}): {}", status, body)
}
other => anyhow::anyhow!("{}", other),
})?;
if is_json_mode() {
let json_resp = JsonResponse::success(
"artifact publish",
PublishJsonResponse {
attestation_rid: body.attestation_rid.clone(),
registry: registry_url.clone(),
package_name: body.package_name.clone(),
signer_did: body.signer_did.clone(),
},
);
json_resp.print()?;
} else {
let out = Output::stdout();
if let Some(ref pkg) = body.package_name {
println!("Anchoring signature for {}...", out.info(pkg));
}
println!(
"{} Cryptographic attestation anchored at {}",
out.success("Success!"),
out.bold(®istry_url)
);
println!("Attestation RID: {}", out.info(&body.attestation_rid));
println!();
if let Some(ref pkg) = body.package_name {
println!(
"View your trust graph online: {}/registry?q={}",
registry_url, pkg
);
}
}
Ok(())
}