/ packages / auths-python / src / artifact_publish.rs
artifact_publish.rs
  1  use std::sync::OnceLock;
  2  
  3  use pyo3::exceptions::{PyConnectionError, PyRuntimeError, PyValueError};
  4  use pyo3::prelude::*;
  5  
  6  use crate::runtime::runtime;
  7  
  8  static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
  9  
 10  fn http_client() -> &'static reqwest::Client {
 11      HTTP_CLIENT.get_or_init(|| {
 12          reqwest::Client::builder()
 13              .connect_timeout(std::time::Duration::from_secs(30))
 14              .timeout(std::time::Duration::from_secs(60))
 15              .build()
 16              .expect("failed to create HTTP client")
 17      })
 18  }
 19  
 20  #[pyclass]
 21  #[derive(Clone)]
 22  pub struct PyArtifactPublishResult {
 23      #[pyo3(get)]
 24      pub attestation_rid: String,
 25      #[pyo3(get)]
 26      pub package_name: Option<String>,
 27      #[pyo3(get)]
 28      pub signer_did: String,
 29  }
 30  
 31  #[pymethods]
 32  impl PyArtifactPublishResult {
 33      fn __repr__(&self) -> String {
 34          let rid_short = if self.attestation_rid.len() > 20 {
 35              format!("{}...", &self.attestation_rid[..20])
 36          } else {
 37              self.attestation_rid.clone()
 38          };
 39          let did_tail = if self.signer_did.len() > 12 {
 40              &self.signer_did[self.signer_did.len() - 12..]
 41          } else {
 42              &self.signer_did
 43          };
 44          let pkg = match &self.package_name {
 45              Some(p) => format!(", pkg={p:?}"),
 46              None => String::new(),
 47          };
 48          format!("ArtifactPublishResult(rid='{rid_short}'{pkg}, signer='…{did_tail}')")
 49      }
 50  }
 51  
 52  /// Publish a signed artifact attestation to a registry.
 53  ///
 54  /// Args:
 55  /// * `attestation_json`: The attestation JSON string from `sign_artifact`.
 56  /// * `registry_url`: Base URL of the target registry.
 57  /// * `package_name`: Optional ecosystem-prefixed package identifier (e.g. `"npm:react@18.3.0"`).
 58  ///
 59  /// Usage:
 60  /// ```ignore
 61  /// let result = publish_artifact(py, att_json, "https://registry.example.com", None)?;
 62  /// println!("Published: {}", result.attestation_rid);
 63  /// ```
 64  #[pyfunction]
 65  #[pyo3(signature = (attestation_json, registry_url, package_name=None))]
 66  pub fn publish_artifact(
 67      py: Python<'_>,
 68      attestation_json: String,
 69      registry_url: String,
 70      package_name: Option<String>,
 71  ) -> PyResult<PyArtifactPublishResult> {
 72      let attestation: serde_json::Value = serde_json::from_str(&attestation_json)
 73          .map_err(|e| PyValueError::new_err(format!("invalid attestation JSON: {e}")))?;
 74  
 75      let url = format!(
 76          "{}/v1/artifacts/publish",
 77          registry_url.trim_end_matches('/')
 78      );
 79  
 80      py.allow_threads(move || {
 81          runtime().block_on(async move {
 82              let mut body = serde_json::json!({ "attestation": attestation });
 83              if let Some(ref name) = package_name {
 84                  body["package_name"] = serde_json::Value::String(name.clone());
 85              }
 86  
 87              let response = http_client()
 88                  .post(&url)
 89                  .json(&body)
 90                  .send()
 91                  .await
 92                  .map_err(|e| PyConnectionError::new_err(format!("registry unreachable: {e}")))?;
 93  
 94              match response.status().as_u16() {
 95                  201 => {
 96                      #[derive(serde::Deserialize)]
 97                      struct PublishResponse {
 98                          attestation_rid: String,
 99                          package_name: Option<String>,
100                          signer_did: String,
101                      }
102                      let resp: PublishResponse = response.json().await.map_err(|e| {
103                          PyRuntimeError::new_err(format!("[AUTHS_NETWORK_ERROR] Invalid registry response: {e}"))
104                      })?;
105                      Ok(PyArtifactPublishResult {
106                          attestation_rid: resp.attestation_rid,
107                          package_name: resp.package_name,
108                          signer_did: resp.signer_did,
109                      })
110                  }
111                  409 => Err(PyRuntimeError::new_err(
112                      "[AUTHS_REGISTRY_ERROR] Duplicate attestation: artifact attestation already published (duplicate RID)",
113                  )),
114                  422 => {
115                      let body = response.text().await.unwrap_or_default();
116                      Err(PyRuntimeError::new_err(format!(
117                          "[AUTHS_VERIFICATION_FAILED] Verification failed: {body}"
118                      )))
119                  }
120                  status => {
121                      let body = response.text().await.unwrap_or_default();
122                      Err(PyRuntimeError::new_err(format!(
123                          "[AUTHS_NETWORK_ERROR] Registry error ({status}): {body}"
124                      )))
125                  }
126              }
127          })
128      })
129  }