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,
}
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::*;
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(),
}
}
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,
}
}
#[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());
}
#[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");
}
}