trazaeo 0.5.6

Open-source provenance SDK and specification for verifiable EO and climate data workflows
Documentation
use crate::c2pa::{input_ref_ingredient, C2paAction, C2paIngredient, C2PA_ACTION_PUBLISHED};
use crate::checkpoint::CheckpointArtifact;
use crate::envelope::{Attestation, PublishEnvelope};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PublishInput {
    pub schema_version: String,
    pub issued_at: String,
    pub subject_id: String,
    pub dataset_id: String,
    pub dataset_version: String,
    pub input_refs: Vec<String>,
    pub output_refs: Vec<String>,
    pub published_artifacts: Vec<CheckpointArtifact>,
    pub primary_artifact_id: String,
    pub checkpoint_manifest_ref: String,
    pub checkpoint_manifest_hash: String,
    pub checkpoint_id: String,
    pub checkpoint_log_root_hash: String,
    pub lineage_refs: Vec<String>,
    pub verification_policy_id: String,
    pub key_id: String,
    pub stac_refs: Vec<String>,
    #[serde(default)]
    pub ogc_refs: Vec<String>,
    #[serde(default)]
    pub c2pa_manifest_ref: Option<String>,
    #[serde(default)]
    pub c2pa_manifest_hash: Option<String>,
    #[serde(default)]
    pub c2pa_ingredients: Vec<C2paIngredient>,
    #[serde(default)]
    pub c2pa_actions: Vec<C2paAction>,
    pub reward_context_ref: Option<String>,
    pub reward_context_hash: Option<String>,
    pub provenance_start_mode: String,
    pub bootstrap_origin_label: Option<String>,
    pub reward_eligible: bool,
}

/// Builds publish envelope.
pub fn build_publish_envelope(input: &PublishInput, attestation: Attestation) -> PublishEnvelope {
    let c2pa_ingredients = if input.c2pa_ingredients.is_empty() {
        input
            .input_refs
            .iter()
            .map(|input_ref| input_ref_ingredient(input_ref))
            .collect()
    } else {
        input.c2pa_ingredients.clone()
    };
    let c2pa_actions = if input.c2pa_actions.is_empty() {
        vec![C2paAction {
            action: C2PA_ACTION_PUBLISHED.to_string(),
            when: input.issued_at.clone(),
            software_agent: input.verification_policy_id.clone(),
            parameters_ref: None,
            parameters_hash: None,
            description: Some(format!("{}/{}", input.dataset_id, input.dataset_version)),
        }]
    } else {
        input.c2pa_actions.clone()
    };

    PublishEnvelope {
        schema_version: input.schema_version.clone(),
        envelope_type: "publish".to_string(),
        issued_at: input.issued_at.clone(),
        subject_id: input.subject_id.clone(),
        dataset_id: input.dataset_id.clone(),
        dataset_version: input.dataset_version.clone(),
        input_refs: input.input_refs.clone(),
        output_refs: input.output_refs.clone(),
        published_artifacts: input.published_artifacts.clone(),
        primary_artifact_id: input.primary_artifact_id.clone(),
        checkpoint_manifest_ref: input.checkpoint_manifest_ref.clone(),
        checkpoint_manifest_hash: input.checkpoint_manifest_hash.clone(),
        checkpoint_id: input.checkpoint_id.clone(),
        checkpoint_log_root_hash: input.checkpoint_log_root_hash.clone(),
        lineage_refs: input.lineage_refs.clone(),
        verification_policy_id: input.verification_policy_id.clone(),
        attestations: vec![attestation],
        key_id: input.key_id.clone(),
        stac_refs: input.stac_refs.clone(),
        ogc_refs: input.ogc_refs.clone(),
        c2pa_manifest_ref: input.c2pa_manifest_ref.clone(),
        c2pa_manifest_hash: input.c2pa_manifest_hash.clone(),
        c2pa_ingredients,
        c2pa_actions,
        reward_context_ref: input.reward_context_ref.clone(),
        reward_context_hash: input.reward_context_hash.clone(),
        provenance_start_mode: input.provenance_start_mode.clone(),
        bootstrap_origin_label: input.bootstrap_origin_label.clone(),
        reward_eligible: input.reward_eligible,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Handles attestation.
    fn attestation() -> Attestation {
        Attestation {
            signer_id: "publisher".to_string(),
            key_id: "key-1".to_string(),
            signature: "sig".to_string(),
            signed_at: "2026-01-01T00:00:00Z".to_string(),
        }
    }

    /// Handles input.
    fn input(reward_eligible: bool) -> PublishInput {
        PublishInput {
            schema_version: "1.0.0".to_string(),
            issued_at: "2026-01-01T00:00:00Z".to_string(),
            subject_id: "publish-1".to_string(),
            dataset_id: "sst".to_string(),
            dataset_version: "v1".to_string(),
            input_refs: vec!["obj://zarr/1".to_string()],
            output_refs: vec!["obj://release/1".to_string()],
            published_artifacts: vec![CheckpointArtifact {
                artifact_id: "artifact-1".to_string(),
                content_root_hash: "root-1".to_string(),
                content_descriptor_ref: None,
                content_descriptor_hash: None,
                media_type: "application/vnd+zarr".to_string(),
            }],
            primary_artifact_id: "artifact-1".to_string(),
            checkpoint_manifest_ref: "checkpoint://1".to_string(),
            checkpoint_manifest_hash: "checkpoint-hash".to_string(),
            checkpoint_id: "checkpoint-1".to_string(),
            checkpoint_log_root_hash: "checkpoint-log-root".to_string(),
            lineage_refs: vec!["capture://1".to_string(), "transform://1".to_string()],
            verification_policy_id: "verify-default".to_string(),
            key_id: "key-1".to_string(),
            stac_refs: vec![],
            ogc_refs: vec![],
            c2pa_manifest_ref: None,
            c2pa_manifest_hash: None,
            c2pa_ingredients: vec![],
            c2pa_actions: vec![],
            reward_context_ref: None,
            reward_context_hash: None,
            provenance_start_mode: "transport_capture".to_string(),
            bootstrap_origin_label: None,
            reward_eligible,
        }
    }

    /// Tests that build publish envelope without reward proof-log linkage is valid when not reward eligible.
    #[test]
    fn build_publish_envelope_without_reward_proof_log_is_valid_when_not_reward_eligible() {
        let env = build_publish_envelope(&input(false), attestation());
        assert!(env.validate().is_ok());
    }

    /// Tests that build publish envelope with reward proof-log linkage is valid.
    #[test]
    fn build_publish_envelope_with_reward_proof_log_is_valid() {
        let env = build_publish_envelope(&input(true), attestation());
        assert!(env.validate().is_ok());
    }

    #[test]
    fn build_publish_envelope_projects_c2pa_action_and_ingredients() {
        let env = build_publish_envelope(&input(false), attestation());

        assert_eq!(env.c2pa_ingredients.len(), 1);
        assert_eq!(env.c2pa_ingredients[0].relationship, "inputTo");
        assert_eq!(env.c2pa_actions.len(), 1);
        assert_eq!(env.c2pa_actions[0].action, "trazaeo.published");
    }
}