Skip to main content

heddle_thread_api/publication/
staging.rs

1//! Shared actual-artifact validation for hosted and owned-device publication.
2//! Original signatures and causal closure are necessary but do not authorize
3//! their authors, current audience, hosted executors, or destination policy.
4use std::io::Read;
5
6use super::PublicationOriginals;
7use crate::{
8    contract::*,
9    fetch::{Error, ValidatedSourceArtifacts},
10};
11
12/// Call on a bounded disk worker after the complete upload. The directory must
13/// contain only the uploaded `source.pack` and `source.idx`; validation never
14/// falls back to objects already installed in a shared store. Dropping either
15/// the input on failure or the result on cancellation removes its scratch.
16pub fn validate_source_artifacts(
17    directory: tempfile::TempDir,
18    opening: &PublishContentOpen,
19    originals: PublicationOriginals,
20) -> Result<ValidatedSourceArtifacts, Error> {
21    validate_source_artifacts_inner(directory, opening, originals, None)
22}
23/// Imported Git roots need independently authenticated enclosing certificates.
24/// The ordinary entry point remains strict when that evidence is unavailable.
25pub fn validate_source_artifacts_with_import_carriers(
26    directory: tempfile::TempDir,
27    opening: &PublishContentOpen,
28    originals: PublicationOriginals,
29    carriers: crypto::import_authority::VerifiedImportCarriers,
30) -> Result<ValidatedSourceArtifacts, Error> {
31    if opening.import_authority.as_ref() != Some(carriers.bundle()) {
32        return Err(Error::HostedTrustRequired);
33    }
34    validate_source_artifacts_inner(directory, opening, originals, Some(carriers))
35}
36fn validate_source_artifacts_inner(
37    directory: tempfile::TempDir,
38    opening: &PublishContentOpen,
39    originals: PublicationOriginals,
40    carriers: Option<crypto::import_authority::VerifiedImportCarriers>,
41) -> Result<ValidatedSourceArtifacts, Error> {
42    originals.validate_bounds().map_err(preparation)?;
43    crate::hybrid::publish_open(opening).map_err(Error::Invalid)?;
44    let thread = opening
45        .thread
46        .as_ref()
47        .ok_or(Error::Invalid("publication Thread absent"))?;
48    let revision = opening
49        .revision
50        .as_ref()
51        .ok_or(Error::Invalid("publication revision absent"))?;
52    if opening.packs.len() != 2
53        || opening.packs[0].kind != pack_extent::Kind::NativePack as i32
54        || opening.packs[1].kind != pack_extent::Kind::NativeIndex as i32
55    {
56        return Err(Error::Invalid("ordered native pack and index required"));
57    }
58    let mut total = 0_u64;
59    for (extent, name) in opening.packs.iter().zip(["source.pack", "source.idx"]) {
60        let address = extent
61            .pack
62            .as_ref()
63            .ok_or(Error::Invalid("artifact address absent"))?;
64        total = total
65            .checked_add(extent.length)
66            .ok_or(Error::Invalid("artifact length overflow"))?;
67        if address.algorithm != "blake3"
68            || address.digest.len() != 32
69            || extent.offset != 0
70            || extent.length == 0
71            || extent.extent_digest.as_ref() != Some(address)
72            || total > 256 * 1024 * 1024
73        {
74            return Err(Error::Invalid("invalid or oversized publication artifact"));
75        }
76        let mut file = std::fs::File::open(directory.path().join(name))?;
77        if file.metadata()?.len() != extent.length {
78            return Err(Error::Invalid("uploaded artifact length differs"));
79        }
80        let mut hasher = blake3::Hasher::new();
81        let mut buffer = [0_u8; 64 * 1024];
82        loop {
83            let read = file.read(&mut buffer)?;
84            if read == 0 {
85                break;
86            }
87            hasher.update(&buffer[..read]);
88        }
89        if hasher.finalize().as_bytes().as_slice() != address.digest {
90            return Err(Error::Invalid("uploaded artifact digest differs"));
91        }
92    }
93    let selected = thread
94        .id
95        .as_ref()
96        .ok_or(Error::Invalid("Thread identity absent"))?;
97    let mut genesis = None;
98    let mut dependencies = Vec::new();
99    for wrapper in originals.geneses {
100        let record = wrapper
101            .genesis
102            .as_ref()
103            .ok_or(Error::Invalid("original genesis absent"))?;
104        let candidate = heddle_object_model::object::thread_replication::ThreadGenesis::decode(
105            &record.canonical_record,
106        )
107        .map_err(preparation)?;
108        if candidate.id().map_err(preparation)?.as_bytes().as_slice() == selected.value {
109            if genesis.replace(wrapper).is_some() {
110                return Err(Error::Invalid("duplicate selected genesis"));
111            }
112        } else {
113            dependencies.push(wrapper);
114        }
115    }
116    let mut operations = Vec::new();
117    let mut receipts = Vec::new();
118    for batch in originals.operations {
119        if batch.import_authority != opening.import_authority
120            || batch.native_authority != opening.native_authority
121        {
122            return Err(Error::Invalid("publication proof differs from opening"));
123        }
124        for received in crate::authority_admission::match_batch(&batch)? {
125            operations.push(received.original);
126            receipts.extend(received.authority_admission);
127        }
128    }
129    let mut validated = crate::fetch::validate_artifacts(
130        directory,
131        thread,
132        revision,
133        &genesis.ok_or(Error::Invalid("original selected genesis absent"))?,
134        operations,
135        dependencies,
136        receipts,
137        carriers,
138    )?;
139    validated.import_authority = opening.import_authority.clone();
140    validated.native_authority = opening.native_authority.clone();
141    Ok(validated)
142}
143fn preparation(error: impl std::fmt::Display) -> Error {
144    Error::Preparation(error.to_string())
145}
146
147/// Structurally verified artifact closure and explicit proposed acceptances.
148/// No acceptance is a current-authority token. The host must authorize every
149/// selected subject and preserve ordinary receipt, claim, and owner-cutoff gates.
150pub struct ProposedSourceArtifacts {
151    artifacts: ValidatedSourceArtifacts,
152    acceptances: super::PublicationAcceptancePlan,
153}
154
155impl ProposedSourceArtifacts {
156    pub fn artifacts(&self) -> &ValidatedSourceArtifacts {
157        &self.artifacts
158    }
159    pub fn acceptances(&self) -> &super::PublicationAcceptancePlan {
160        &self.acceptances
161    }
162    pub fn into_parts(self) -> (ValidatedSourceArtifacts, super::PublicationAcceptancePlan) {
163        (self.artifacts, self.acceptances)
164    }
165}
166
167pub fn validate_proposed_source_artifacts(
168    directory: tempfile::TempDir,
169    opening: &PublishContentClientFrame,
170    originals: PublicationOriginals,
171    spool_genesis: heddle_object_model::object::ContentHash,
172) -> Result<ProposedSourceArtifacts, Error> {
173    let (originals, acceptances) =
174        super::proposed_publication(opening, originals, spool_genesis).map_err(preparation)?;
175    let Some(publish_content_client_frame::Body::Open(open)) = &opening.body else {
176        return Err(Error::Invalid("publication Open required"));
177    };
178    let artifacts = validate_source_artifacts(directory, open, originals)?;
179    Ok(ProposedSourceArtifacts {
180        artifacts,
181        acceptances,
182    })
183}