Skip to main content

auths_sdk/workflows/
artifact.rs

1//! Artifact digest computation and publishing workflow.
2
3use auths_core::ports::network::{NetworkError, RateLimitInfo, RegistryClient};
4use auths_verifier::core::ResourceId;
5use serde::Deserialize;
6use thiserror::Error;
7
8use crate::ports::artifact::{ArtifactDigest, ArtifactError, ArtifactSource};
9
10/// Configuration for publishing an artifact attestation to a registry.
11///
12/// Args:
13/// * `attestation`: The signed attestation JSON.
14/// * `package_name`: Optional ecosystem-prefixed package identifier (e.g. `"npm:react@18.3.0"`).
15/// * `registry_url`: Base URL of the target registry.
16pub struct ArtifactPublishConfig {
17    /// The signed attestation JSON payload.
18    pub attestation: serde_json::Value,
19    /// Optional ecosystem-prefixed package identifier (e.g. `"npm:react@18.3.0"`).
20    pub package_name: Option<String>,
21    /// Base URL of the target registry (trailing slash stripped by the SDK).
22    pub registry_url: String,
23}
24
25/// Response from a successful artifact publish.
26#[derive(Debug, Deserialize)]
27pub struct ArtifactPublishResult {
28    /// Stable registry identifier for the stored attestation.
29    pub attestation_rid: ResourceId,
30    /// Package identifier echoed back by the registry, if provided.
31    pub package_name: Option<String>,
32    /// DID of the identity that signed the attestation.
33    pub signer_did: String,
34    /// Rate limit information from response headers, if the registry provides it.
35    #[serde(skip)]
36    pub rate_limit: Option<RateLimitInfo>,
37}
38
39/// Errors that can occur when publishing an artifact attestation.
40#[derive(Debug, Error)]
41pub enum ArtifactPublishError {
42    /// Registry rejected the attestation because an identical RID already exists.
43    #[error("artifact attestation already published (duplicate RID)")]
44    DuplicateAttestation,
45    /// Registry could not verify the attestation signature.
46    #[error("signature verification failed at registry: {0}")]
47    VerificationFailed(String),
48    /// Registry returned an unexpected HTTP status code.
49    #[error("registry error ({status}): {body}")]
50    RegistryError {
51        /// HTTP status code returned by the registry.
52        status: u16,
53        /// Response body text from the registry.
54        body: String,
55    },
56    /// Network-level error communicating with the registry.
57    #[error("network error: {0}")]
58    Network(#[from] NetworkError),
59    /// Failed to serialize the publish request body.
60    #[error("failed to serialize publish request: {0}")]
61    Serialize(String),
62    /// Failed to deserialize the registry response.
63    #[error("failed to deserialize registry response: {0}")]
64    Deserialize(String),
65}
66
67/// Publish a signed artifact attestation to a registry.
68///
69/// Args:
70/// * `config`: Attestation payload, optional package name, and registry URL.
71/// * `registry`: Registry HTTP client implementing `RegistryClient`.
72///
73/// Usage:
74/// ```ignore
75/// let result = publish_artifact(&config, &registry_client).await?;
76/// println!("RID: {}", result.attestation_rid);
77/// ```
78pub async fn publish_artifact<R: RegistryClient>(
79    config: &ArtifactPublishConfig,
80    registry: &R,
81) -> Result<ArtifactPublishResult, ArtifactPublishError> {
82    let mut body = serde_json::json!({ "attestation": config.attestation });
83    if let Some(ref name) = config.package_name {
84        body["package_name"] = serde_json::Value::String(name.clone());
85    }
86    let json_bytes =
87        serde_json::to_vec(&body).map_err(|e| ArtifactPublishError::Serialize(e.to_string()))?;
88
89    let response = registry
90        .post_json(&config.registry_url, "v1/artifacts", &json_bytes)
91        .await?;
92
93    match response.status {
94        201 => {
95            let mut result: ArtifactPublishResult = serde_json::from_slice(&response.body)
96                .map_err(|e| ArtifactPublishError::Deserialize(e.to_string()))?;
97            result.rate_limit = response.rate_limit;
98            Ok(result)
99        }
100        409 => Err(ArtifactPublishError::DuplicateAttestation),
101        422 => {
102            let body = String::from_utf8_lossy(&response.body).into_owned();
103            Err(ArtifactPublishError::VerificationFailed(body))
104        }
105        status => {
106            let body = String::from_utf8_lossy(&response.body).into_owned();
107            Err(ArtifactPublishError::RegistryError { status, body })
108        }
109    }
110}
111
112/// Compute the digest of an artifact source.
113///
114/// Args:
115/// * `source`: Any implementation of `ArtifactSource`.
116///
117/// Usage:
118/// ```ignore
119/// let digest = compute_digest(&file_artifact)?;
120/// println!("sha256:{}", digest.hex);
121/// ```
122pub fn compute_digest(source: &dyn ArtifactSource) -> Result<ArtifactDigest, ArtifactError> {
123    source.digest()
124}
125
126/// Verify an artifact attestation against an expected signer DID.
127///
128/// Symmetric to `sign_artifact()` — given the attestation JSON and the
129/// expected signer's DID, verifies the signature is valid.
130///
131/// Args:
132/// * `attestation_json`: The attestation JSON string.
133/// * `signer_did`: Expected signer DID (`did:keri:` or `did:key:`).
134/// * `provider`: Crypto backend for Ed25519 verification.
135///
136/// Usage:
137/// ```ignore
138/// let result = verify_artifact(&json, "did:key:z6Mk...", &provider).await?;
139/// assert!(result.valid);
140/// ```
141pub async fn verify_artifact<R: RegistryClient>(
142    config: &ArtifactVerifyConfig,
143    registry: &R,
144) -> Result<ArtifactVerifyResult, ArtifactPublishError> {
145    let body = serde_json::json!({
146        "attestation": config.attestation_json,
147        "issuer_key": config.signer_did,
148    });
149    let json_bytes =
150        serde_json::to_vec(&body).map_err(|e| ArtifactPublishError::Serialize(e.to_string()))?;
151
152    let response = registry
153        .post_json(&config.registry_url, "v1/verify", &json_bytes)
154        .await?;
155
156    match response.status {
157        200 => {
158            let result: ArtifactVerifyResult = serde_json::from_slice(&response.body)
159                .map_err(|e| ArtifactPublishError::Deserialize(e.to_string()))?;
160            Ok(result)
161        }
162        status => {
163            let body = String::from_utf8_lossy(&response.body).into_owned();
164            Err(ArtifactPublishError::RegistryError { status, body })
165        }
166    }
167}
168
169/// Configuration for verifying an artifact attestation.
170pub struct ArtifactVerifyConfig {
171    /// The attestation JSON to verify.
172    pub attestation_json: String,
173    /// Expected signer DID.
174    pub signer_did: String,
175    /// Registry URL for verification.
176    pub registry_url: String,
177}
178
179/// Result of artifact verification.
180#[derive(Debug, Deserialize)]
181pub struct ArtifactVerifyResult {
182    /// Whether the attestation verified successfully.
183    pub valid: bool,
184    /// The signer DID extracted from the attestation (if valid).
185    pub signer_did: Option<String>,
186}