Skip to main content

heddle_api/
import_authority.rs

1//! HYBRID canonical import authority. The existing heddle capability verifier
2//! supplies independently selected owner history/current permission. This
3//! module verifies the NEW typed parent grant and its bounded job child;
4//! it neither enrolls incoming roots nor substitutes for owner verification.
5use crate::heddle::api::v1alpha2::*;
6pub use crate::hybrid_codec::{Reject, strict_decode};
7use crate::hybrid_codec::{canonical, field, hash, key_id, record, signing_digest, verify, width};
8
9pub const PERMISSION_DOMAIN: &str = "heddle-import-member-permission-v1";
10pub const GENESIS_DOMAIN: &str = "heddle-import-genesis-authority-v1";
11pub const DELEGATION_DOMAIN: &str = "heddle-import-job-delegation-v1";
12pub const RENEWAL_DOMAIN: &str = "heddle-import-job-renewal-v1";
13pub const OPERATION_DOMAIN: &str = "heddle-delegated-import-operation-v1";
14pub const MANIFEST_DOMAIN: &str = "heddle-import-result-manifest-v1";
15pub const PUBLICATION_DOMAIN: &str = "heddle-import-publication-payload-v1";
16pub const MAX_BRANCHES: usize = 256;
17pub const MAX_RECORD_BYTES: usize = 64 * 1024;
18pub const MAX_BUNDLE_BYTES: usize = 1024 * 1024;
19pub const MAX_RESULT_BYTES: u64 = 1 << 30;
20pub const CANCELLATION_NAMESPACE: &str = "heddle-import-cancel-v1";
21
22record!(AuthorizationSignature, signer_key_id:b, signature:b);
23record!(RecordSignature, public_key:b, signature:b);
24record!(SignedRecord, format:s, canonical_record:b, signatures:l);
25record!(ImportFrontierV1, format_version:u, thread_id:b, operation_ids:h);
26record!(ImportContentV1, format_version:u, canonical_capture:b);
27record!(ImportBoundaryAcceptanceV1, binding:m, signed_acceptance:m, originals_manifest:b, publication_intent:b, original_receipts:l);
28record!(ImportGenesisWitnessV1, format_version:u, binding:m, original_genesis:m, creator_authority_envelope:b, boundary_acceptance:o);
29record!(ImportAuthorityWitnessV1, format_version:u, kind:e, original:m, dependencies:l, authority_envelope:b, boundary_acceptances:l);
30record!(HostedLandingRequestProofV1, format_version:u, signing_identity:s, method_path:s, timestamp_millis:u, nonce:b, request_body:b, signature:m);
31record!(HostedLandingWitnessV1, format_version:u, execution:m, request:m, source_operation:m, review_evidence:l, authority_envelope:b);
32record!(ImportJobCasStateV1, format_version:u, logical_job_id:b, retry_lineage_id:b, active_predecessor:m, authority_epoch:u, committed_manifest:m);
33record!(ImportIdentityV1, spool_uuid:b, spool_genesis_digest:b, owner_id:b,
34    owner_account_uuid:b, owner_state_hash:b, ownership_transfer_sequence:u);
35record!(ImportOwnerChainV1, spool_genesis_digest:b, owner_state_hashes:q, transfer_audit_hashes:q);
36record!(ImportBranchLimitV1, ref_name:s, hash_algorithm:e, ref_mode:e, pinned_commit_oid:b,
37    genesis_digest:b, target_thread_id:b, expected_frontier_digest:b, slot_id:u, max_result_bytes:u, ref_disclosure:e);
38record!(ImportPermissionScopeV1, provider:s, source_url:s, branches:l, destination_version:b,
39    options_digest:b, converter_version:s, max_operations:u, max_result_bytes:u);
40record!(ImportMemberPermissionV1, format_version:u, identity:m, logical_job_id:b,
41    retry_lineage_id:b, subject_public_key:b, purpose:e, scope:m, not_before_unix_seconds:u,
42    expires_at_unix_seconds:u, cancellation_id:b, owner_chain_digest:b, nonce:b);
43record!(SignedImportMemberPermissionV1, body:m, owner_signature:m);
44record!(ImportGenesisAuthorityV1, format_version:u, identity:m, genesis_digest:b,
45    original_creator_signature:b, creator_public_key:b, creator_authority_envelope_digest:b,
46    parent_permission_digest:b, owner_chain_digest:b);
47record!(SignedImportGenesisAuthorityV1, body:m, creator_signature:m);
48record!(ImportBranchManifestV1, limit:m, genesis_authority_digest:b);
49record!(ImportJobDelegationV1, format_version:u, identity:m, delegation_id:b, logical_job_id:b,
50    retry_lineage_id:b, job_public_key:b, job_key_id:b, delegating_public_key:b,
51    parent_permission_digest:b, owner_chain_digest:b, purpose:e, scope:m, branch_manifest:l,
52    not_before_unix_seconds:u, expires_at_unix_seconds:u, cancellation_id:b, predecessor_delegation_digest:b);
53record!(ImportJobPreparationV1, format_version:u, identity:m, delegation_id:b, logical_job_id:b,
54    retry_lineage_id:b, job_public_key:b, job_key_id:b, owner_chain_digest:b, purpose:e,
55    scope:m, cancellation_id:b, predecessor_delegation_digest:b);
56record!(SignedImportJobDelegationV1, body:m, delegating_signature:m);
57record!(ImportCommittedSlotV1, ref_name:s, slot_id:u, signed_operation_digest:b,
58    resulting_frontier_digest:b, result_bytes:u);
59record!(ImportResultManifestV1, format_version:u, logical_job_id:b, retry_lineage_id:b, slots:l);
60record!(ImportJobRenewalV1, format_version:u, predecessor_delegation_digest:b,
61    expected_authority_epoch:u, committed_manifest_digest:b, replacement:m);
62record!(SignedImportJobRenewalV1, body:m, delegating_signature:m);
63record!(DelegatedImportOperationV1, format_version:u, spool_uuid:b, spool_genesis_digest:b,
64    logical_job_id:b, retry_lineage_id:b, physical_operation_id:b, delegation_digest:b,
65    ref_name:s, slot_id:u, hash_algorithm:e, observed_commit_oid:b, genesis_digest:b,
66    target_thread_id:b, expected_frontier_digest:b, resulting_frontier_digest:b,
67    resulting_content_digest:b, result_bytes:u, options_digest:b, converter_version:s);
68record!(SignedDelegatedImportOperationV1, body:m, job_signature:m);
69record!(ImportPublicationWitnessV1, format_version:u, signed_operation_digest:b, delegation_digest:b,
70    logical_job_id:b, retry_lineage_id:b, physical_operation_id:b, ref_name:s, slot_id:u,
71    hash_algorithm:e, observed_commit_oid:b, expected_frontier_digest:b, resulting_frontier_digest:b,
72    terminal_manifest_digest:b);
73
74pub fn signed_permission_digest(v: &SignedImportMemberPermissionV1) -> Result<Vec<u8>, Reject> {
75    signing_digest("heddle-signed-import-member-permission-v1", v)
76}
77pub fn owner_chain_digest(v: &ImportOwnerChainV1) -> Result<Vec<u8>, Reject> {
78    width(&v.spool_genesis_digest, 32)?;
79    if v.owner_state_hashes.is_empty()
80        || v.owner_state_hashes.len() > 64
81        || v.transfer_audit_hashes.len() > 64
82    {
83        return Err(Reject::Bounds);
84    }
85    for h in v.owner_state_hashes.iter().chain(&v.transfer_audit_hashes) {
86        width(h, 32)?;
87    }
88    if v.owner_state_hashes.windows(2).any(|w| w[0] >= w[1]) {
89        return Err(Reject::Canonical);
90    }
91    signing_digest("heddle-import-owner-chain-v1", v)
92}
93pub fn signed_genesis_digest(v: &SignedImportGenesisAuthorityV1) -> Result<Vec<u8>, Reject> {
94    signing_digest("heddle-signed-import-genesis-authority-v1", v)
95}
96pub fn signed_delegation_digest(v: &SignedImportJobDelegationV1) -> Result<Vec<u8>, Reject> {
97    signing_digest("heddle-signed-import-job-delegation-v1", v)
98}
99pub fn signed_operation_digest(v: &SignedDelegatedImportOperationV1) -> Result<Vec<u8>, Reject> {
100    signing_digest("heddle-signed-delegated-import-operation-v1", v)
101}
102pub fn manifest_digest(v: &ImportResultManifestV1) -> Result<Vec<u8>, Reject> {
103    signing_digest(MANIFEST_DOMAIN, v)
104}
105pub fn verify_authorization_signature(
106    key: &[u8],
107    domain: &str,
108    body: &impl crate::hybrid_codec::Canonical,
109    signature: &AuthorizationSignature,
110) -> Result<(), Reject> {
111    if signature.signer_key_id != key_id(key) {
112        return Err(Reject::Signature);
113    }
114    let bytes = canonical(body)?;
115    if bytes.len() > MAX_RECORD_BYTES {
116        return Err(Reject::Bounds);
117    }
118    verify(
119        key,
120        &hash(&[domain.as_bytes(), &bytes]),
121        &signature.signature,
122    )
123}
124
125/// Conservative canonical URL grammar for v1: lowercase DNS HTTPS host, no
126/// userinfo/query/fragment/port/escapes, ASCII unreserved path segments. No
127/// implicit URL normalization is performed by a signing implementation.
128pub fn canonical_https(value: &str, origin: bool) -> Result<(), Reject> {
129    if value.len() > 2048 {
130        return Err(Reject::Bounds);
131    }
132    let rest = value.strip_prefix("https://").ok_or(Reject::Canonical)?;
133    let (host, path) = rest.split_once('/').unwrap_or((rest, ""));
134    if host.is_empty()
135        || host.len() > 253
136        || host.split('.').any(|part| {
137            part.is_empty()
138                || part.len() > 63
139                || part.starts_with('-')
140                || part.ends_with('-')
141                || !part
142                    .bytes()
143                    .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
144        })
145        || (origin && rest != host)
146        || (!origin && path.is_empty())
147        || path.split('/').any(|p| {
148            p.is_empty() && !origin
149                || p == "."
150                || p == ".."
151                || !p
152                    .bytes()
153                    .all(|b| b.is_ascii_alphanumeric() || b"-._~".contains(&b))
154        })
155    {
156        return Err(Reject::Canonical);
157    }
158    Ok(())
159}
160fn identity(value: &ImportIdentityV1) -> Result<(), Reject> {
161    for v in [&value.spool_uuid, &value.owner_account_uuid] {
162        width(v, 16)?;
163        if v.iter().all(|b| *b == 0) {
164            return Err(Reject::Canonical);
165        }
166    }
167    for v in [
168        &value.spool_genesis_digest,
169        &value.owner_id,
170        &value.owner_state_hash,
171    ] {
172        width(v, 32)?;
173    }
174    Ok(())
175}
176fn interval(start: i64, end: i64, now: i64) -> Result<(), Reject> {
177    if start < 0 || end <= start {
178        return Err(Reject::Semantic);
179    }
180    if now < start || now >= end {
181        return Err(Reject::Expired);
182    }
183    Ok(())
184}
185// Structural validation never substitutes a synthetic historical clock.
186fn validity(start: i64, end: i64, now: i64, current: bool) -> Result<(), Reject> {
187    if start < 0 || end <= start {
188        return Err(Reject::Semantic);
189    }
190    if current {
191        interval(start, end, now)?;
192    }
193    Ok(())
194}
195fn branch(value: &ImportBranchLimitV1) -> Result<(), Reject> {
196    if !value.ref_name.starts_with("refs/heads/")
197        || value.ref_name.len() > 1024
198        || value.ref_name.ends_with('/')
199        || value.ref_name.ends_with('.')
200        || value.ref_name.contains("..")
201        || value.ref_name.contains("//")
202        || value.ref_name.contains("@{")
203        || value
204            .ref_name
205            .split('/')
206            .any(|p| p.starts_with('.') || p.ends_with(".lock"))
207        || !value
208            .ref_name
209            .bytes()
210            .all(|b| b.is_ascii_alphanumeric() || b"/_-.".contains(&b))
211    {
212        return Err(Reject::Canonical);
213    }
214    let size = match value.hash_algorithm {
215        1 => 20,
216        2 => 32,
217        _ => return Err(Reject::Version),
218    };
219    match value.ref_mode {
220        1 => {
221            width(&value.pinned_commit_oid, size)?;
222            if value.ref_disclosure != 0 {
223                return Err(Reject::RefDisclosure);
224            }
225        }
226        2 if value.pinned_commit_oid.is_empty() => {
227            if value.ref_disclosure != 1 {
228                return Err(Reject::RefDisclosure);
229            }
230        }
231        _ => return Err(Reject::Semantic),
232    }
233    for v in [
234        &value.genesis_digest,
235        &value.target_thread_id,
236        &value.expected_frontier_digest,
237    ] {
238        width(v, 32)?;
239    }
240    if value.max_result_bytes == 0 || value.max_result_bytes > MAX_RESULT_BYTES {
241        return Err(Reject::Bounds);
242    }
243    Ok(())
244}
245
246/// Check independently observed OID knowledge before signing/preparation.
247/// Empty/unknown is None, never an implicit observe-mode selection or consent.
248pub fn validate_ref_selection(
249    value: &ImportBranchLimitV1,
250    known_commit_oid: Option<&[u8]>,
251) -> Result<(), Reject> {
252    branch(value)?;
253    if let Some(oid) = known_commit_oid {
254        width(oid, if value.hash_algorithm == 1 { 20 } else { 32 })?;
255        if value.ref_mode != 1 || value.pinned_commit_oid != oid {
256            return Err(Reject::RefPinning);
257        }
258    }
259    Ok(())
260}
261
262/// Converter option octets are selected verbatim from authenticated discovery.
263pub fn conversion_options_digest(version: &str, options: &[u8]) -> Result<Vec<u8>, Reject> {
264    if version.is_empty() || version.len() > 128 || !version.is_ascii() {
265        return Err(Reject::Canonical);
266    }
267    if options.len() > 4096 {
268        return Err(Reject::Bounds);
269    }
270    let mut bytes = Vec::new();
271    crate::hybrid_codec::counted(&mut bytes, version.as_bytes())?;
272    crate::hybrid_codec::counted(&mut bytes, options)?;
273    Ok(hash(&[b"heddle-import-conversion-options-v1", &bytes]))
274}
275
276/// Resolve explicit custody using a CURRENT authenticated connection provider.
277/// Public URLs never select an adapter by domain. Network/grant checks remain host gates.
278pub fn resolve_import_provider(
279    source: &ProviderRepository,
280    connection_provider: Option<&str>,
281) -> Result<&'static str, Reject> {
282    canonical_https(&source.clone_url, false)?;
283    if source.provider_repository_id.len() > 4096 || source.name.len() > 4096 {
284        return Err(Reject::Bounds);
285    }
286    if let Some(connection) = &source.connection {
287        let positive = |v: &str| {
288            !v.is_empty()
289                && v.bytes().all(|b| b.is_ascii_digit())
290                && v.parse::<u64>().is_ok_and(|id| id > 0)
291        };
292        if connection_provider != Some("github")
293            || connection.spool.is_some()
294            || connection.id.is_empty()
295            || !positive(&source.provider_repository_id)
296            || !positive(&source.installation_id)
297        {
298            return Err(Reject::SourceSelection);
299        }
300        let path = source
301            .clone_url
302            .strip_prefix("https://github.com/")
303            .ok_or(Reject::SourceSelection)?;
304        let parts: Vec<_> = path.split('/').collect();
305        if parts.len() != 2
306            || parts[0].is_empty()
307            || parts[1].strip_suffix(".git").is_none_or(str::is_empty)
308        {
309            return Err(Reject::SourceSelection);
310        }
311        Ok("github")
312    } else {
313        if connection_provider.is_some()
314            || source.private
315            || !source.installation_id.is_empty()
316            || (!source.provider_repository_id.is_empty()
317                && source.provider_repository_id != source.clone_url)
318        {
319            return Err(Reject::SourceSelection);
320        }
321        Ok("public-git")
322    }
323}
324
325/// Discovery may report unknown. Preparing/signing requires known=true; no SHA-1 fallback.
326pub fn validate_repository_hash_algorithm(
327    source: &ProviderRepository,
328    known: bool,
329) -> Result<(), Reject> {
330    let size = match source.hash_algorithm {
331        1 => Some(40),
332        2 => Some(64),
333        0 if !known => None,
334        _ => return Err(Reject::Version),
335    };
336    if source.refs.len() > 512 {
337        return Err(Reject::Bounds);
338    }
339    for (i, r) in source.refs.iter().enumerate() {
340        if r.hash_algorithm != source.hash_algorithm {
341            return Err(Reject::SourceSelection);
342        }
343        if i > 0 && source.refs[i - 1].name >= r.name {
344            return Err(Reject::Canonical);
345        }
346        if !r.head_oid.is_empty() {
347            let size = size.ok_or(Reject::Version)?;
348            if r.head_oid.len() != size {
349                return Err(Reject::SourceSelection);
350            }
351            if !r
352                .head_oid
353                .bytes()
354                .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
355            {
356                return Err(Reject::Canonical);
357            }
358        }
359    }
360    Ok(())
361}
362
363/// Compare independent repository discovery before signing, including known-OID pinning.
364pub fn validate_discovered_import_scope(
365    scope: &ImportPermissionScopeV1,
366    source: &ProviderRepository,
367) -> Result<(), Reject> {
368    validate_discovered_import_scope_inner(scope, source, false)
369}
370
371fn validate_discovered_import_scope_inner(
372    scope: &ImportPermissionScopeV1,
373    source: &ProviderRepository,
374    retained: bool,
375) -> Result<(), Reject> {
376    validate_repository_hash_algorithm(source, true)?;
377    if scope.source_url != source.clone_url {
378        return Err(Reject::SourceSelection);
379    }
380    for b in &scope.branches {
381        if b.hash_algorithm != source.hash_algorithm {
382            return Err(Reject::SourceSelection);
383        }
384        let oid = source
385            .refs
386            .iter()
387            .find(|r| r.name == b.ref_name)
388            .filter(|r| !r.head_oid.is_empty())
389            .map(|r| hex::decode(&r.head_oid).map_err(|_| Reject::Canonical))
390            .transpose()?;
391        // An authenticated retained pin selects its original commit, not today's head.
392        validate_ref_selection(
393            b,
394            if retained && b.ref_mode == 1 {
395                None
396            } else {
397                oid.as_deref()
398            },
399        )?;
400    }
401    Ok(())
402}
403
404/// Host resolves the selector anew and checks current grants and selected-commit
405/// availability before issuing a reservation, including renewals. Retained state
406/// MUST come from an authenticated job-state read or receiver-owned durable state;
407/// the opaque predecessor binds its independently verified authority to that CAS.
408pub fn prepare_import_source_scope(
409    request: &PrepareImportJobRequest,
410    current_source: &ProviderRepository,
411    connection_provider: Option<&str>,
412    configuration: &GetImportConfigurationResponse,
413    current_destination_version: &[u8],
414    retained: Option<(&VerifiedImportRenewalPredecessor, &ImportJobCasStateV1)>,
415) -> Result<ImportPermissionScopeV1, Reject> {
416    let selector = request.source.as_ref().ok_or(Reject::SourceSelection)?;
417    if selector.connection != current_source.connection
418        || (selector.provider_repository_id != current_source.provider_repository_id
419            && (selector.connection.is_some() || !selector.provider_repository_id.is_empty()))
420        || selector.installation_id != current_source.installation_id
421        || selector.private != current_source.private
422    {
423        return Err(Reject::SourceSelection);
424    }
425    let scope = request.proposed_scope.as_ref().ok_or(Reject::Canonical)?;
426    let provider = resolve_import_provider(current_source, connection_provider)?;
427    if scope.provider != provider {
428        return Err(Reject::SourceSelection);
429    }
430    if let Some((predecessor, state)) = retained {
431        validate_cas_state(state)?;
432        let previous = &predecessor.previous.body;
433        let identity = previous.identity.as_ref().ok_or(Reject::Canonical)?;
434        if signing_digest("heddle-import-job-cas-state-v1", state)? != predecessor.state_digest
435            || request.renew_logical_job_id != previous.logical_job_id
436            || request.retry_lineage_id != previous.retry_lineage_id
437            || request.destination.as_ref().is_none_or(|s| {
438                initial_operation_id(&identity.spool_uuid, false).map_or(true, |id| s.id != id)
439            })
440        {
441            return Err(Reject::StaleContext);
442        }
443        let mut selected = scope.clone();
444        if selected.destination_version.is_empty() {
445            selected.destination_version = current_destination_version.to_vec();
446        }
447        remaining_scope(
448            &selected,
449            previous.scope.as_ref().ok_or(Reject::Canonical)?,
450            state.committed_manifest.as_ref().ok_or(Reject::Canonical)?,
451        )?;
452    } else if !request.renew_logical_job_id.is_empty() {
453        return Err(Reject::StaleContext);
454    }
455    validate_discovered_import_scope_inner(scope, current_source, retained.is_some())?;
456    prepare_scope(scope, configuration, current_destination_version)
457}
458
459fn validate_provider_support(
460    provider: &str,
461    configuration: &GetImportConfigurationResponse,
462) -> Result<(), Reject> {
463    if !configuration
464        .providers
465        .iter()
466        .any(|p| p.provider == provider)
467    {
468        return Err(Reject::SourceSelection);
469    }
470    Ok(())
471}
472
473pub fn validate_import_configuration(v: &GetImportConfigurationResponse) -> Result<(), Reject> {
474    use prost::Message;
475    if v.encoded_len() > MAX_BUNDLE_BYTES || v.converters.is_empty() || v.converters.len() > 32 {
476        return Err(Reject::Bounds);
477    }
478    for (i, c) in v.converters.iter().enumerate() {
479        for text in [&c.converter_version, &c.options_encoding] {
480            if text.is_empty() || text.len() > 128 || !text.is_ascii() {
481                return Err(Reject::Canonical);
482            }
483        }
484        if i > 0 && v.converters[i - 1].converter_version >= c.converter_version {
485            return Err(Reject::Canonical);
486        }
487        if c.canonical_options.is_empty()
488            || c.canonical_options.len() > 64
489            || c.canonical_options.iter().any(|o| o.len() > 4096)
490        {
491            return Err(Reject::Bounds);
492        }
493        if c.canonical_options.windows(2).any(|w| w[0] >= w[1])
494            || !c.canonical_options.contains(&c.default_options)
495        {
496            return Err(Reject::Canonical);
497        }
498    }
499    if v.providers.is_empty() || v.providers.len() > 2 {
500        return Err(Reject::Bounds);
501    }
502    for (i, p) in v.providers.iter().enumerate() {
503        if i > 0 && v.providers[i - 1].provider >= p.provider {
504            return Err(Reject::Canonical);
505        }
506        let mode = match p.provider.as_str() {
507            "github" => 1,
508            "public-git" => 2,
509            _ => return Err(Reject::SourceSelection),
510        };
511        if p.source_modes != [mode] {
512            return Err(Reject::SourceSelection);
513        }
514    }
515    if let Some(default) = &v.default_converter_version
516        && !v.converters.iter().any(|c| &c.converter_version == default)
517    {
518        return Err(Reject::Canonical);
519    }
520    let l = v.limits.as_ref().ok_or(Reject::Canonical)?;
521    if l.max_branches == 0
522        || l.max_branches as usize > MAX_BRANCHES
523        || l.max_operations == 0
524        || l.max_operations as usize > MAX_BRANCHES
525        || l.max_result_bytes == 0
526        || l.max_result_bytes > MAX_RESULT_BYTES
527        || l.max_branch_result_bytes == 0
528        || l.max_branch_result_bytes > l.max_result_bytes
529    {
530        return Err(Reject::Bounds);
531    }
532    Ok(())
533}
534
535/// Host-side negotiation with a CURRENT authenticated configuration and CAS.
536/// Only an empty destination token is filled; all other choices survive exactly.
537pub fn prepare_scope(
538    proposed: &ImportPermissionScopeV1,
539    configuration: &GetImportConfigurationResponse,
540    current_destination_version: &[u8],
541) -> Result<ImportPermissionScopeV1, Reject> {
542    use ImportPreparationRefusalReason as Reason;
543    validate_import_configuration(configuration)?;
544    width(current_destination_version, 32)?;
545    if !proposed.destination_version.is_empty() && proposed.destination_version.len() != 32 {
546        return Err(Reject::PreparationRefused(Reason::InvalidScope));
547    }
548    if !proposed.destination_version.is_empty()
549        && proposed.destination_version != current_destination_version
550    {
551        return Err(Reject::PreparationRefused(Reason::DestinationConflict));
552    }
553    let mut selected = proposed.clone();
554    selected.destination_version = current_destination_version.to_vec();
555    validate_scope(&selected).map_err(|_| Reject::PreparationRefused(Reason::InvalidScope))?;
556    validate_provider_support(&selected.provider, configuration)
557        .map_err(|_| Reject::PreparationRefused(Reason::InvalidScope))?;
558    let converter = configuration
559        .converters
560        .iter()
561        .find(|c| c.converter_version == selected.converter_version)
562        .ok_or(Reject::PreparationRefused(Reason::UnsupportedConverter))?;
563    let supported = converter
564        .canonical_options
565        .iter()
566        .try_fold(false, |found, o| {
567            Ok::<_, Reject>(
568                found
569                    || conversion_options_digest(&converter.converter_version, o)?
570                        == selected.options_digest,
571            )
572        })?;
573    if !supported {
574        return Err(Reject::PreparationRefused(Reason::UnsupportedOptions));
575    }
576    let limits = configuration.limits.as_ref().ok_or(Reject::Canonical)?;
577    if selected.branches.len() > limits.max_branches as usize
578        || selected.max_operations > limits.max_operations
579        || selected.max_result_bytes > limits.max_result_bytes
580        || selected
581            .branches
582            .iter()
583            .any(|b| b.max_result_bytes > limits.max_branch_result_bytes)
584    {
585        return Err(Reject::PreparationRefused(Reason::BudgetExceeded));
586    }
587    Ok(selected)
588}
589
590/// Browser-side comparison before signing. A host cannot silently negotiate.
591pub fn validate_preparation_response(
592    request: &PrepareImportJobRequest,
593    response: &PrepareImportJobResponse,
594) -> Result<(), Reject> {
595    if let Some(refusal) = &response.refusal {
596        let reason = ImportPreparationRefusalReason::try_from(refusal.reason)
597            .map_err(|_| Reject::Version)?;
598        if reason == ImportPreparationRefusalReason::Unspecified
599            || refusal.field.len() > 256
600            || !refusal.field.is_ascii()
601            || response.proposal.is_some()
602            || response.renewal_state.is_some()
603            || response.reservation_expires_at_unix_seconds != 0
604            || response.prepared_at_unix_seconds != 0
605            || response.max_validity_duration_seconds != 0
606            || response.clock_skew_allowance_seconds != 0
607        {
608            return Err(Reject::Canonical);
609        }
610        return Err(Reject::PreparationRefused(reason));
611    }
612    let p = response.proposal.as_ref().ok_or(Reject::Canonical)?;
613    let returned = p.scope.as_ref().ok_or(Reject::Canonical)?;
614    let mut requested = request.proposed_scope.clone().ok_or(Reject::Canonical)?;
615    if requested.destination_version.is_empty() {
616        requested.destination_version = returned.destination_version.clone();
617    }
618    if canonical(&requested)? != canonical(returned)?
619        || p.identity != request.identity
620        || p.retry_lineage_id != request.retry_lineage_id
621    {
622        return Err(Reject::PreparedFields);
623    }
624    validate_scope(returned)?;
625    if request.renew_logical_job_id.is_empty() {
626        if response.renewal_state.is_some()
627            || p.predecessor_delegation_digest.iter().any(|b| *b != 0)
628        {
629            return Err(Reject::PreparedFields);
630        }
631    } else {
632        if p.logical_job_id != request.renew_logical_job_id {
633            return Err(Reject::PreparedFields);
634        }
635        validate_renewal_preparation(response)?;
636    }
637    Ok(())
638}
639
640/// Source association/provider comes from the host's authenticated resolver,
641/// never a projection hint. Native base decoding/identity and current grants
642/// remain host gates; this validates carrier, bounds and exact originals.
643pub fn validate_commit_request(
644    request: &CommitImportJobRequest,
645    resolved_provider: &str,
646    current_source: &ProviderRepository,
647    configuration: &GetImportConfigurationResponse,
648) -> Result<(), Reject> {
649    use prost::Message;
650    let source = request.source.as_ref().ok_or(Reject::SourceSelection)?;
651    let proof = request.proof.as_ref().ok_or(Reject::Canonical)?;
652    if request.client_operation_id.is_empty()
653        || request.client_operation_id.len() > 128
654        || request.destination.as_ref().is_none_or(|d| d.id.is_empty())
655    {
656        return Err(Reject::Canonical);
657    }
658    if request.initial_base_state.len() > 4096 || proof.encoded_len() > MAX_BUNDLE_BYTES {
659        return Err(Reject::Bounds);
660    }
661    if proof.format_version != 1
662        || proof.delegations.len() != 1
663        || !proof.renewals.is_empty()
664        || !proof.operations.is_empty()
665        || proof.terminal_manifest.is_some()
666        || !proof.manifests.is_empty()
667    {
668        return Err(Reject::Canonical);
669    }
670    let d = proof.delegations[0]
671        .body
672        .as_ref()
673        .ok_or(Reject::Canonical)?;
674    let scope = d.scope.as_ref().ok_or(Reject::Canonical)?;
675    validate_scope(scope)?;
676    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
677    identity(id)?;
678    let uuid = hex::encode(&id.spool_uuid);
679    let destination_id = format!(
680        "{}-{}-{}-{}-{}",
681        &uuid[..8],
682        &uuid[8..12],
683        &uuid[12..16],
684        &uuid[16..20],
685        &uuid[20..]
686    );
687    if request
688        .destination
689        .as_ref()
690        .is_none_or(|s| s.id != destination_id)
691    {
692        return Err(Reject::Scope);
693    }
694    if d.predecessor_delegation_digest != [0; 32] {
695        return Err(Reject::Canonical);
696    }
697    if source.clone_url != scope.source_url
698        || resolved_provider != scope.provider
699        || source.provider_repository_id.len() > 4096
700        || source.name.len() > 4096
701    {
702        return Err(Reject::SourceSelection);
703    }
704    if source.connection != current_source.connection
705        || (source.provider_repository_id != current_source.provider_repository_id
706            && (source.connection.is_some() || !source.provider_repository_id.is_empty()))
707        || source.clone_url != current_source.clone_url
708        || source.installation_id != current_source.installation_id
709        || source.private != current_source.private
710    {
711        return Err(Reject::SourceSelection);
712    }
713    let provider = resolve_import_provider(
714        current_source,
715        current_source
716            .connection
717            .as_ref()
718            .map(|_| resolved_provider),
719    )?;
720    if provider != resolved_provider {
721        return Err(Reject::SourceSelection);
722    }
723    validate_import_configuration(configuration)?;
724    validate_provider_support(provider, configuration)?;
725    validate_repository_hash_algorithm(current_source, true)?;
726    if source.hash_algorithm != current_source.hash_algorithm {
727        return Err(Reject::SourceSelection);
728    }
729    // A frozen pin names the selected commit, even if the branch head moves.
730    // OBSERVE still cannot hide a currently known selected OID by clearing hints.
731    for b in &scope.branches {
732        if b.hash_algorithm != current_source.hash_algorithm {
733            return Err(Reject::SourceSelection);
734        }
735        if b.ref_mode == 2
736            && current_source
737                .refs
738                .iter()
739                .any(|r| r.name == b.ref_name && !r.head_oid.is_empty())
740        {
741            return Err(Reject::RefPinning);
742        }
743    }
744    // Current converter/options/budget support is also rechecked at activation.
745    prepare_scope(scope, configuration, &scope.destination_version)?;
746    if proof.original_geneses.len() != scope.branches.len()
747        || proof.creator_authority_envelopes.len() != scope.branches.len()
748        || proof.genesis_authorities.len() != scope.branches.len()
749        || d.branch_manifest.len() != scope.branches.len()
750    {
751        return Err(Reject::GenesisBinding);
752    }
753    // Arrays are ordered exactly like the signed branch manifest, no second
754    // association-by-name payload and no incoming regenerated branch originals.
755    for (i, b) in scope.branches.iter().enumerate() {
756        let original = &proof.original_geneses[i];
757        let binding = &proof.genesis_authorities[i];
758        let g = binding.body.as_ref().ok_or(Reject::GenesisBinding)?;
759        let m = &d.branch_manifest[i];
760        if native_id(original) != b.genesis_digest
761            || g.genesis_digest != b.genesis_digest
762            || m.limit.as_ref() != Some(b)
763            || m.genesis_authority_digest != signed_genesis_digest(binding)?
764            || g.creator_authority_envelope_digest != hash(&[&proof.creator_authority_envelopes[i]])
765            || proof.creator_authority_envelopes[i].is_empty()
766            || proof.creator_authority_envelopes[i].len() > MAX_RECORD_BYTES
767            || original.signatures.len() != 1
768            || original.signatures[0].public_key != g.creator_public_key
769            || original.signatures[0].signature != g.original_creator_signature
770        {
771            return Err(Reject::GenesisBinding);
772        }
773        verify_native(original, "heddle-thread-genesis-v1")?;
774    }
775    Ok(())
776}
777
778/// This closed route has no initial-submission or authority-attachment role.
779pub fn validate_import_source(_: &ImportSourceRequest) -> Result<(), Reject> {
780    Err(Reject::ImportSourceRequiresCommit)
781}
782
783/// Complete initial validation, excluding native model/owner history, live
784/// source access and transaction checks owned by the hosted implementation.
785pub fn verify_commit_submission(
786    request: &CommitImportJobRequest,
787    prepared: &PrepareImportJobResponse,
788    resolved_provider: &str,
789    current_source: &ProviderRepository,
790    configuration: &GetImportConfigurationResponse,
791    expected: &ImportOwnerExpectation<'_>,
792) -> Result<VerifiedImportDelegation, Reject> {
793    validate_commit_request(request, resolved_provider, current_source, configuration)?;
794    let proof = request.proof.as_ref().ok_or(Reject::Canonical)?;
795    let member = proof
796        .member_permission
797        .as_ref()
798        .or(proof.member_permissions.first());
799    if proof.member_permissions.len() > 1
800        || proof
801            .member_permissions
802            .first()
803            .is_some_and(|p| Some(p) != member)
804    {
805        return Err(Reject::ImportPermission);
806    }
807    verify_prepared_delegation(
808        prepared,
809        &proof.delegations[0],
810        member,
811        &proof.genesis_authorities,
812        expected,
813    )
814}
815
816pub fn validate_commit_response(
817    request: &CommitImportJobRequest,
818    response: &MutationResponse,
819) -> Result<(), Reject> {
820    let receipt = response.receipt.as_ref().ok_or(Reject::PendingOperation)?;
821    let Some(mutation_receipt::Outcome::PendingOperation(operation)) = &receipt.outcome else {
822        return Err(Reject::PendingOperation);
823    };
824    if request.client_operation_id.is_empty()
825        || receipt.client_operation_id != request.client_operation_id
826        || request.destination.is_none()
827        || operation.spool != request.destination
828        || operation.id
829            != initial_operation_id(
830                &request
831                    .proof
832                    .as_ref()
833                    .ok_or(Reject::PendingOperation)?
834                    .delegations
835                    .first()
836                    .and_then(|d| d.body.as_ref())
837                    .ok_or(Reject::PendingOperation)?
838                    .retry_lineage_id,
839                false,
840            )?
841    {
842        return Err(Reject::PendingOperation);
843    }
844    Ok(())
845}
846
847/// Use a durable caller-scoped idempotency row BEFORE rechecking expired job
848/// authority. Host stores the original request/receipt atomically with activation.
849pub fn check_commit_replay(
850    request: &CommitImportJobRequest,
851    stored: &CommitImportJobRequest,
852    response: &MutationResponse,
853) -> Result<(), Reject> {
854    if request != stored {
855        return Err(Reject::OperationIdReused);
856    }
857    validate_commit_response(request, response)
858}
859pub fn validate_scope(value: &ImportPermissionScopeV1) -> Result<(), Reject> {
860    canonical_https(&value.source_url, false)?;
861    if value.provider.is_empty()
862        || value.provider.len() > 64
863        || !value
864            .provider
865            .bytes()
866            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
867        || value.converter_version.is_empty()
868        || value.converter_version.len() > 128
869        || !value.converter_version.is_ascii()
870    {
871        return Err(Reject::Canonical);
872    }
873    width(&value.destination_version, 32)?;
874    width(&value.options_digest, 32)?;
875    if value.branches.is_empty()
876        || value.branches.len() > MAX_BRANCHES
877        || value.max_operations == 0
878        || value.max_operations as usize > MAX_BRANCHES
879        || value.max_result_bytes == 0
880        || value.max_result_bytes > MAX_RESULT_BYTES
881    {
882        return Err(Reject::Bounds);
883    }
884    if (value.max_operations as usize) < value.branches.len() {
885        return Err(Reject::Scope);
886    }
887    let mut total = 0_u64;
888    for (i, b) in value.branches.iter().enumerate() {
889        branch(b)?;
890        if i > 0 && value.branches[i - 1].ref_name.as_bytes() >= b.ref_name.as_bytes() {
891            return Err(Reject::Canonical);
892        }
893        total = total
894            .checked_add(b.max_result_bytes)
895            .ok_or(Reject::Bounds)?;
896    }
897    if total > value.max_result_bytes {
898        return Err(Reject::Scope);
899    }
900    Ok(())
901}
902fn scope_subset(child: &ImportPermissionScopeV1, parent: &ImportPermissionScopeV1) -> bool {
903    child.provider == parent.provider
904        && child.source_url == parent.source_url
905        && child.destination_version == parent.destination_version
906        && child.options_digest == parent.options_digest
907        && child.converter_version == parent.converter_version
908        && child.max_operations <= parent.max_operations
909        && child.max_result_bytes <= parent.max_result_bytes
910        && child.branches.iter().all(|c| {
911            parent.branches.iter().any(|p| {
912                let mut limit = c.clone();
913                limit.max_result_bytes = p.max_result_bytes;
914                limit == *p && c.max_result_bytes <= p.max_result_bytes
915            })
916        })
917}
918
919/// Public context from the existing owner/keyring verifier, not from fields in
920/// the incoming bundle. now is receiver/host time for new work or independently
921/// verified witness observation time for retained history, NEVER author time.
922pub struct ImportOwnerExpectation<'a> {
923    pub identity: &'a ImportIdentityV1,
924    pub owner_public_key: &'a [u8],
925    pub owner_chain_digest: &'a [u8],
926    pub authority_expires_at_seconds: i64,
927    pub now_unix_seconds: i64,
928    pub forbidden_job_keys: &'a [Vec<u8>], // Every user/root/witness key, including tombstones.
929    pub known_job_associations: &'a [(Vec<u8>, Vec<u8>)], // key -> logical job.
930}
931pub fn verify_member_permission(
932    signed: &SignedImportMemberPermissionV1,
933    expected: &ImportOwnerExpectation<'_>,
934) -> Result<(), Reject> {
935    verify_member_permission_inner(signed, expected, true)
936}
937fn verify_member_permission_inner(
938    signed: &SignedImportMemberPermissionV1,
939    expected: &ImportOwnerExpectation<'_>,
940    current: bool,
941) -> Result<(), Reject> {
942    let p = signed.body.as_ref().ok_or(Reject::ImportPermission)?;
943    if p.format_version != 1 || p.purpose != 1 {
944        return Err(Reject::ImportPermission);
945    }
946    identity(p.identity.as_ref().ok_or(Reject::Canonical)?)?;
947    if p.identity.as_ref() != Some(expected.identity)
948        || p.owner_chain_digest != expected.owner_chain_digest
949    {
950        return Err(Reject::Root);
951    }
952    width(&p.logical_job_id, 16)?;
953    width(&p.retry_lineage_id, 16)?;
954    width(&p.subject_public_key, 32)?;
955    width(&p.cancellation_id, 32)?;
956    width(&p.nonce, 32)?;
957    width(&p.owner_chain_digest, 32)?;
958    validate_scope(p.scope.as_ref().ok_or(Reject::Canonical)?)?;
959    validity(
960        p.not_before_unix_seconds,
961        p.expires_at_unix_seconds,
962        expected.now_unix_seconds,
963        current,
964    )?;
965    if p.expires_at_unix_seconds > expected.authority_expires_at_seconds {
966        return Err(Reject::Scope);
967    }
968    verify_authorization_signature(
969        expected.owner_public_key,
970        PERMISSION_DOMAIN,
971        p,
972        signed.owner_signature.as_ref().ok_or(Reject::Signature)?,
973    )
974}
975
976#[derive(Debug, Clone, PartialEq)]
977pub struct VerifiedImportDelegation {
978    body: ImportJobDelegationV1,
979    digest: Vec<u8>,
980    member: Option<SignedImportMemberPermissionV1>,
981}
982impl VerifiedImportDelegation {
983    pub fn body(&self) -> &ImportJobDelegationV1 {
984        &self.body
985    }
986    pub fn digest(&self) -> &[u8] {
987        &self.digest
988    }
989}
990pub fn verify_delegation(
991    signed: &SignedImportJobDelegationV1,
992    member: Option<&SignedImportMemberPermissionV1>,
993    expected: &ImportOwnerExpectation<'_>,
994) -> Result<VerifiedImportDelegation, Reject> {
995    verify_delegation_inner(signed, member, expected, true)
996}
997fn verify_delegation_inner(
998    signed: &SignedImportJobDelegationV1,
999    member: Option<&SignedImportMemberPermissionV1>,
1000    expected: &ImportOwnerExpectation<'_>,
1001    current: bool,
1002) -> Result<VerifiedImportDelegation, Reject> {
1003    let d = signed.body.as_ref().ok_or(Reject::Canonical)?;
1004    if d.format_version != 1 || d.purpose != 1 {
1005        return Err(Reject::Version);
1006    }
1007    identity(d.identity.as_ref().ok_or(Reject::Canonical)?)?;
1008    if d.identity.as_ref() != Some(expected.identity)
1009        || d.owner_chain_digest != expected.owner_chain_digest
1010    {
1011        return Err(Reject::Root);
1012    }
1013    for v in [&d.delegation_id, &d.logical_job_id, &d.retry_lineage_id] {
1014        width(v, 16)?;
1015        if v.iter().all(|b| *b == 0) {
1016            return Err(Reject::Canonical);
1017        }
1018    }
1019    for v in [
1020        &d.job_public_key,
1021        &d.job_key_id,
1022        &d.delegating_public_key,
1023        &d.parent_permission_digest,
1024        &d.owner_chain_digest,
1025        &d.cancellation_id,
1026        &d.predecessor_delegation_digest,
1027    ] {
1028        width(v, 32)?;
1029    }
1030    if d.job_key_id != key_id(&d.job_public_key) {
1031        return Err(Reject::Canonical);
1032    }
1033    if d.job_public_key == d.delegating_public_key
1034        || d.job_public_key == expected.owner_public_key
1035        || expected.forbidden_job_keys.contains(&d.job_public_key)
1036    {
1037        return Err(Reject::KeyRole);
1038    }
1039    if expected
1040        .known_job_associations
1041        .iter()
1042        .any(|(k, j)| k == &d.job_public_key && j != &d.logical_job_id)
1043    {
1044        return Err(Reject::Scope);
1045    }
1046    let scope = d.scope.as_ref().ok_or(Reject::Canonical)?;
1047    validate_scope(scope)?;
1048    if d.branch_manifest.len() != scope.branches.len() {
1049        return Err(Reject::Scope);
1050    }
1051    for (m, b) in d.branch_manifest.iter().zip(&scope.branches) {
1052        width(&m.genesis_authority_digest, 32)?;
1053        if m.limit.as_ref() != Some(b) {
1054            return Err(Reject::Scope);
1055        }
1056    }
1057    validity(
1058        d.not_before_unix_seconds,
1059        d.expires_at_unix_seconds,
1060        expected.now_unix_seconds,
1061        current,
1062    )?;
1063    if d.expires_at_unix_seconds > expected.authority_expires_at_seconds {
1064        return Err(Reject::Scope);
1065    }
1066    if d.delegating_public_key == expected.owner_public_key {
1067        if member.is_some() || d.parent_permission_digest != vec![0; 32] {
1068            return Err(Reject::ImportPermission);
1069        }
1070    } else {
1071        let member = member.ok_or(Reject::ImportPermission)?;
1072        verify_member_permission_inner(member, expected, current)?;
1073        let p = member.body.as_ref().ok_or(Reject::ImportPermission)?;
1074        if d.parent_permission_digest != signed_permission_digest(member)?
1075            || d.delegating_public_key != p.subject_public_key
1076            || d.logical_job_id != p.logical_job_id
1077            || d.retry_lineage_id != p.retry_lineage_id
1078            || d.not_before_unix_seconds < p.not_before_unix_seconds
1079            || d.expires_at_unix_seconds > p.expires_at_unix_seconds
1080            || !scope_subset(scope, p.scope.as_ref().ok_or(Reject::Canonical)?)
1081        {
1082            return Err(Reject::Scope);
1083        }
1084    }
1085    verify_authorization_signature(
1086        &d.delegating_public_key,
1087        DELEGATION_DOMAIN,
1088        d,
1089        signed
1090            .delegating_signature
1091            .as_ref()
1092            .ok_or(Reject::Signature)?,
1093    )?;
1094    Ok(VerifiedImportDelegation {
1095        body: d.clone(),
1096        digest: signed_delegation_digest(signed)?,
1097        member: member.cloned(),
1098    })
1099}
1100/// Frozen canonical projection. The browser completes only the fields absent
1101/// here; it cannot normalize or reduce even an otherwise authorized scope.
1102pub fn delegation_preparation(d: &ImportJobDelegationV1) -> ImportJobPreparationV1 {
1103    ImportJobPreparationV1 {
1104        format_version: d.format_version,
1105        identity: d.identity.clone(),
1106        delegation_id: d.delegation_id.clone(),
1107        logical_job_id: d.logical_job_id.clone(),
1108        retry_lineage_id: d.retry_lineage_id.clone(),
1109        job_public_key: d.job_public_key.clone(),
1110        job_key_id: d.job_key_id.clone(),
1111        owner_chain_digest: d.owner_chain_digest.clone(),
1112        purpose: d.purpose,
1113        scope: d.scope.clone(),
1114        cancellation_id: d.cancellation_id.clone(),
1115        predecessor_delegation_digest: d.predecessor_delegation_digest.clone(),
1116    }
1117}
1118
1119/// Validate Commit against the HOST-STORED preparation and independently
1120/// selected current authority. Native genesis/envelope verification, online
1121/// revocation, custody uniqueness and transactional activation remain host gates.
1122/// Retained renewal genesis bindings require their original accepted context.
1123pub fn verify_prepared_delegation(
1124    prepared: &PrepareImportJobResponse,
1125    signed: &SignedImportJobDelegationV1,
1126    member: Option<&SignedImportMemberPermissionV1>,
1127    geneses: &[SignedImportGenesisAuthorityV1],
1128    expected: &ImportOwnerExpectation<'_>,
1129) -> Result<VerifiedImportDelegation, Reject> {
1130    verify_prepared_inner(prepared, signed, member, geneses, expected, false)
1131}
1132/// Browser signing preflight only: no execution or admission token is returned.
1133pub fn preflight_prepared_delegation(
1134    prepared: &PrepareImportJobResponse,
1135    signed: &SignedImportJobDelegationV1,
1136    member: Option<&SignedImportMemberPermissionV1>,
1137    geneses: &[SignedImportGenesisAuthorityV1],
1138    expected: &ImportOwnerExpectation<'_>,
1139) -> Result<(), Reject> {
1140    verify_prepared_inner(prepared, signed, member, geneses, expected, true).map(|_| ())
1141}
1142fn verify_prepared_inner(
1143    prepared: &PrepareImportJobResponse,
1144    signed: &SignedImportJobDelegationV1,
1145    member: Option<&SignedImportMemberPermissionV1>,
1146    geneses: &[SignedImportGenesisAuthorityV1],
1147    expected: &ImportOwnerExpectation<'_>,
1148    browser: bool,
1149) -> Result<VerifiedImportDelegation, Reject> {
1150    let proposal = prepared.proposal.as_ref().ok_or(Reject::Canonical)?;
1151    let d = signed.body.as_ref().ok_or(Reject::Canonical)?;
1152    if canonical(proposal)? != canonical(&delegation_preparation(d))? {
1153        return Err(Reject::PreparedFields);
1154    }
1155    let scope = proposal.scope.as_ref().ok_or(Reject::Canonical)?;
1156    if d.branch_manifest.len() != scope.branches.len() {
1157        return Err(Reject::PreparedFields);
1158    }
1159    for (m, b) in d.branch_manifest.iter().zip(&scope.branches) {
1160        let limit = m.limit.as_ref().ok_or(Reject::PreparedFields)?;
1161        if canonical(limit)? != canonical(b)? {
1162            return Err(Reject::PreparedFields);
1163        }
1164    }
1165    // Use i128 for host arithmetic so extreme advertised uint64 bounds cannot
1166    // wrap. Skew permits a future not-before, never grace after expiry.
1167    let now = i128::from(expected.now_unix_seconds);
1168    let start = i128::from(d.not_before_unix_seconds);
1169    let end = i128::from(d.expires_at_unix_seconds);
1170    let at = i128::from(prepared.prepared_at_unix_seconds);
1171    let skew = i128::from(prepared.clock_skew_allowance_seconds);
1172    if at < 0
1173        || now < 0
1174        || if browser { now + skew < at } else { now < at }
1175        || i128::from(prepared.reservation_expires_at_unix_seconds) != at + 3600
1176        || now >= i128::from(prepared.reservation_expires_at_unix_seconds)
1177    {
1178        return Err(Reject::Expired);
1179    }
1180    if prepared.max_validity_duration_seconds == 0
1181        || start < 0
1182        || start < at - skew
1183        || start > now + skew
1184        || end <= start
1185        || end <= now
1186        || end - start > i128::from(prepared.max_validity_duration_seconds)
1187    {
1188        return Err(Reject::ValidityBounds);
1189    }
1190    if let Some(parent) = member {
1191        if browser {
1192            let p = parent.body.as_ref().ok_or(Reject::ImportPermission)?;
1193            verify_member_permission_inner(parent, expected, false)?;
1194            if i128::from(p.not_before_unix_seconds) > now + skew
1195                || i128::from(p.expires_at_unix_seconds) <= now
1196            {
1197                return Err(Reject::Expired);
1198            }
1199        } else {
1200            verify_member_permission(parent, expected)?;
1201        }
1202    }
1203    // Future not-before within skew can be committed, but verify_new_operation
1204    // still refuses execution until that exact signed time. Parent/owner expiry
1205    // and containment are checked without extending them by skew.
1206    let at_start = ImportOwnerExpectation {
1207        now_unix_seconds: expected.now_unix_seconds.max(d.not_before_unix_seconds),
1208        ..*expected
1209    };
1210    let verified = verify_delegation_inner(
1211        signed,
1212        member,
1213        if browser { expected } else { &at_start },
1214        !browser,
1215    )?;
1216    if geneses.len() != d.branch_manifest.len() {
1217        return Err(Reject::GenesisBinding);
1218    }
1219    for m in &d.branch_manifest {
1220        let branch = m.limit.as_ref().ok_or(Reject::Canonical)?;
1221        let g = geneses
1222            .iter()
1223            .find(|g| signed_genesis_digest(g).is_ok_and(|h| h == m.genesis_authority_digest))
1224            .ok_or(Reject::GenesisBinding)?;
1225        let body = g.body.as_ref().ok_or(Reject::GenesisBinding)?;
1226        if body.genesis_digest != branch.genesis_digest {
1227            return Err(Reject::GenesisBinding);
1228        }
1229        if d.predecessor_delegation_digest.iter().all(|b| *b == 0) {
1230            verify_genesis_authority(
1231                g,
1232                &verified,
1233                &branch.genesis_digest,
1234                &body.original_creator_signature,
1235                &body.creator_authority_envelope_digest,
1236            )?;
1237        }
1238    }
1239    Ok(verified)
1240}
1241
1242pub fn verify_genesis_authority(
1243    signed: &SignedImportGenesisAuthorityV1,
1244    delegation: &VerifiedImportDelegation,
1245    original_genesis_digest: &[u8],
1246    original_signature: &[u8],
1247    envelope_digest: &[u8],
1248) -> Result<(), Reject> {
1249    let g = signed.body.as_ref().ok_or(Reject::Canonical)?;
1250    let d = &delegation.body;
1251    if g.format_version != 1 {
1252        return Err(Reject::Version);
1253    }
1254    width(&g.original_creator_signature, 64)?;
1255    for v in [
1256        &g.genesis_digest,
1257        &g.creator_public_key,
1258        &g.creator_authority_envelope_digest,
1259        &g.parent_permission_digest,
1260        &g.owner_chain_digest,
1261    ] {
1262        width(v, 32)?;
1263    }
1264    if g.identity != d.identity
1265        || g.creator_public_key != d.delegating_public_key
1266        || g.parent_permission_digest != d.parent_permission_digest
1267        || g.owner_chain_digest != d.owner_chain_digest
1268        || g.genesis_digest != original_genesis_digest
1269        || g.original_creator_signature != original_signature
1270        || g.creator_authority_envelope_digest != envelope_digest
1271        || !d.branch_manifest.iter().any(|m| {
1272            m.limit
1273                .as_ref()
1274                .is_some_and(|b| b.genesis_digest == g.genesis_digest)
1275                && signed_genesis_digest(signed).is_ok_and(|h| h == m.genesis_authority_digest)
1276        })
1277    {
1278        return Err(Reject::Scope);
1279    }
1280    verify_authorization_signature(
1281        &g.creator_public_key,
1282        GENESIS_DOMAIN,
1283        g,
1284        signed.creator_signature.as_ref().ok_or(Reject::Signature)?,
1285    )
1286}
1287/// Structural signature + scoped operation only. This does not establish
1288/// publication, current policy, cancellation, leases or conversion correctness.
1289pub fn verify_operation(
1290    signed: &SignedDelegatedImportOperationV1,
1291    delegation: &VerifiedImportDelegation,
1292) -> Result<(), Reject> {
1293    let o = signed.body.as_ref().ok_or(Reject::Canonical)?;
1294    let d = &delegation.body;
1295    if o.format_version != 1 {
1296        return Err(Reject::Version);
1297    }
1298    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
1299    width(&o.physical_operation_id, 16)?;
1300    for v in [
1301        &o.spool_genesis_digest,
1302        &o.delegation_digest,
1303        &o.genesis_digest,
1304        &o.target_thread_id,
1305        &o.expected_frontier_digest,
1306        &o.resulting_frontier_digest,
1307        &o.resulting_content_digest,
1308        &o.options_digest,
1309    ] {
1310        width(v, 32)?;
1311    }
1312    width(&o.spool_uuid, 16)?;
1313    width(&o.logical_job_id, 16)?;
1314    width(&o.retry_lineage_id, 16)?;
1315    let scope = d.scope.as_ref().ok_or(Reject::Canonical)?;
1316    let b = scope
1317        .branches
1318        .iter()
1319        .find(|b| b.ref_name == o.ref_name && b.slot_id == o.slot_id)
1320        .ok_or(Reject::Scope)?;
1321    let oid_len = match o.hash_algorithm {
1322        1 => 20,
1323        2 => 32,
1324        _ => return Err(Reject::Version),
1325    };
1326    width(&o.observed_commit_oid, oid_len)?;
1327    if o.spool_uuid != id.spool_uuid
1328        || o.spool_genesis_digest != id.spool_genesis_digest
1329        || o.logical_job_id != d.logical_job_id
1330        || o.retry_lineage_id != d.retry_lineage_id
1331        || o.delegation_digest != delegation.digest
1332        || o.hash_algorithm != b.hash_algorithm
1333        || (b.ref_mode == 1 && o.observed_commit_oid != b.pinned_commit_oid)
1334        || o.genesis_digest != b.genesis_digest
1335        || o.target_thread_id != b.target_thread_id
1336        || o.expected_frontier_digest != b.expected_frontier_digest
1337        || o.result_bytes > b.max_result_bytes
1338        || o.result_bytes == 0
1339        || o.options_digest != scope.options_digest
1340        || o.converter_version != scope.converter_version
1341    {
1342        return Err(Reject::Scope);
1343    }
1344    verify_authorization_signature(
1345        &d.job_public_key,
1346        OPERATION_DOMAIN,
1347        o,
1348        signed.job_signature.as_ref().ok_or(Reject::Signature)?,
1349    )
1350}
1351pub fn verify_new_operation(
1352    signed: &SignedDelegatedImportOperationV1,
1353    delegation: &VerifiedImportDelegation,
1354    now_seconds: i64,
1355) -> Result<(), Reject> {
1356    interval(
1357        delegation.body.not_before_unix_seconds,
1358        delegation.body.expires_at_unix_seconds,
1359        now_seconds,
1360    )?;
1361    verify_operation(signed, delegation)
1362}
1363pub fn validate_manifest(m: &ImportResultManifestV1) -> Result<(), Reject> {
1364    if m.format_version != 1 {
1365        return Err(Reject::Version);
1366    }
1367    width(&m.logical_job_id, 16)?;
1368    width(&m.retry_lineage_id, 16)?;
1369    if m.slots.len() > MAX_BRANCHES {
1370        return Err(Reject::Bounds);
1371    }
1372    for (i, s) in m.slots.iter().enumerate() {
1373        width(&s.signed_operation_digest, 32)?;
1374        width(&s.resulting_frontier_digest, 32)?;
1375        if !s.ref_name.starts_with("refs/heads/") || !s.ref_name.is_ascii() {
1376            return Err(Reject::Canonical);
1377        }
1378        if s.ref_name.len() > 1024 || s.result_bytes == 0 || s.result_bytes > MAX_RESULT_BYTES {
1379            return Err(Reject::Bounds);
1380        }
1381        if i > 0 && (&m.slots[i - 1].ref_name, m.slots[i - 1].slot_id) >= (&s.ref_name, s.slot_id) {
1382            return Err(Reject::Canonical);
1383        }
1384    }
1385    Ok(())
1386}
1387/// CAS state MUST be receiver-owned and held under the publication/renewal
1388/// transaction fence. Returns replacement only after all remaining-slot checks.
1389pub fn verify_renewal(
1390    signed: &SignedImportJobRenewalV1,
1391    previous: &VerifiedImportDelegation,
1392    committed: &ImportResultManifestV1,
1393    authority_epoch: u64,
1394    member: Option<&SignedImportMemberPermissionV1>,
1395    expected: &ImportOwnerExpectation<'_>,
1396) -> Result<VerifiedImportDelegation, Reject> {
1397    let r = signed.body.as_ref().ok_or(Reject::Canonical)?;
1398    if r.format_version != 1 {
1399        return Err(Reject::Version);
1400    }
1401    validate_manifest(committed)?;
1402    if r.expected_authority_epoch != authority_epoch {
1403        return Err(Reject::StaleContext);
1404    }
1405    if r.predecessor_delegation_digest != previous.digest {
1406        return Err(Reject::RenewalFork);
1407    }
1408    if r.committed_manifest_digest != manifest_digest(committed)? {
1409        return Err(Reject::StaleManifest);
1410    }
1411    let signed_next = r.replacement.as_ref().ok_or(Reject::Canonical)?;
1412    let next = verify_delegation(signed_next, member, expected)?;
1413    let before = &previous.body;
1414    let after = &next.body;
1415    let before_id = before.identity.as_ref().ok_or(Reject::Canonical)?;
1416    let after_id = after.identity.as_ref().ok_or(Reject::Canonical)?;
1417    if after.logical_job_id != before.logical_job_id
1418        || after.retry_lineage_id != before.retry_lineage_id
1419        || committed.logical_job_id != before.logical_job_id
1420        || committed.retry_lineage_id != before.retry_lineage_id
1421        || after_id.spool_uuid != before_id.spool_uuid
1422        || after_id.spool_genesis_digest != before_id.spool_genesis_digest
1423        || after.predecessor_delegation_digest != previous.digest
1424        || after.job_public_key == before.job_public_key
1425        || after.delegation_id == before.delegation_id
1426    {
1427        return Err(Reject::RenewalFork);
1428    }
1429    let old_scope = before.scope.as_ref().ok_or(Reject::Canonical)?;
1430    let new_scope = after.scope.as_ref().ok_or(Reject::Canonical)?;
1431    if !scope_subset(new_scope, old_scope) {
1432        return Err(Reject::RenewalFork);
1433    }
1434    let old_slots = &old_scope.branches;
1435    let mut consumed = 0_u64;
1436    for slot in &committed.slots {
1437        // Old certificates may already omit previously committed slots. Only
1438        // charge all committed slots against the original logical-job budgets;
1439        // the host's initial manifest/limits remain durable across renewals.
1440        if let Some(b) = old_slots
1441            .iter()
1442            .find(|b| b.ref_name == slot.ref_name && b.slot_id == slot.slot_id)
1443        {
1444            if slot.result_bytes > b.max_result_bytes {
1445                return Err(Reject::RenewalFork);
1446            }
1447            consumed = consumed
1448                .checked_add(slot.result_bytes)
1449                .ok_or(Reject::Bounds)?;
1450        }
1451        if new_scope
1452            .branches
1453            .iter()
1454            .any(|b| b.ref_name == slot.ref_name && b.slot_id == slot.slot_id)
1455        {
1456            return Err(Reject::CommittedSlot);
1457        }
1458    }
1459    let removed = old_slots
1460        .iter()
1461        .filter(|b| {
1462            committed
1463                .slots
1464                .iter()
1465                .any(|s| s.ref_name == b.ref_name && s.slot_id == b.slot_id)
1466        })
1467        .count();
1468    if new_scope.max_operations as usize > old_scope.max_operations as usize - removed
1469        || new_scope.max_result_bytes
1470            > old_scope
1471                .max_result_bytes
1472                .checked_sub(consumed)
1473                .ok_or(Reject::RenewalFork)?
1474        || after.branch_manifest.iter().any(|m| {
1475            !before.branch_manifest.iter().any(|old| {
1476                old.genesis_authority_digest == m.genesis_authority_digest
1477                    && old
1478                        .limit
1479                        .as_ref()
1480                        .zip(m.limit.as_ref())
1481                        .is_some_and(|(a, b)| {
1482                            a.ref_name == b.ref_name && a.genesis_digest == b.genesis_digest
1483                        })
1484            })
1485        })
1486    {
1487        return Err(Reject::RenewalFork);
1488    }
1489    if let Some(parent) = member {
1490        let p = parent.body.as_ref().ok_or(Reject::ImportPermission)?;
1491        remaining_scope(
1492            p.scope.as_ref().ok_or(Reject::Canonical)?,
1493            old_scope,
1494            committed,
1495        )?;
1496        if let Some(old_parent) = &previous.member {
1497            let old = old_parent.body.as_ref().ok_or(Reject::ImportPermission)?;
1498            if p.subject_public_key == old.subject_public_key
1499                && p.logical_job_id == old.logical_job_id
1500                && p.retry_lineage_id == old.retry_lineage_id
1501                && (p.cancellation_id != old.cancellation_id
1502                    || (parent != old_parent && p.nonce == old.nonce))
1503            {
1504                return Err(Reject::ImportPermission);
1505            }
1506        }
1507    }
1508    verify_authorization_signature(
1509        &after.delegating_public_key,
1510        RENEWAL_DOMAIN,
1511        r,
1512        signed
1513            .delegating_signature
1514            .as_ref()
1515            .ok_or(Reject::Signature)?,
1516    )?;
1517    Ok(next)
1518}
1519/// Persistent unique slot identity: logical job/ref/slot. Exact replay returns
1520/// the old receipt/manifest, never another publication or fresh witness.
1521pub fn check_slot_replay(
1522    committed: &ImportResultManifestV1,
1523    signed: &SignedDelegatedImportOperationV1,
1524) -> Result<bool, Reject> {
1525    validate_manifest(committed)?;
1526    let o = signed.body.as_ref().ok_or(Reject::Canonical)?;
1527    if committed.logical_job_id != o.logical_job_id
1528        || committed.retry_lineage_id != o.retry_lineage_id
1529    {
1530        return Err(Reject::Scope);
1531    }
1532    match committed
1533        .slots
1534        .iter()
1535        .find(|s| s.ref_name == o.ref_name && s.slot_id == o.slot_id)
1536    {
1537        Some(s)
1538            if s.signed_operation_digest == signed_operation_digest(signed)?
1539                && s.resulting_frontier_digest == o.resulting_frontier_digest
1540                && s.result_bytes == o.result_bytes =>
1541        {
1542            Ok(true)
1543        }
1544        Some(_) => Err(Reject::SlotConflict),
1545        None => Ok(false),
1546    }
1547}
1548/// Verify exact committed publication in addition to job signature/scope. The
1549/// owner/keyring verifier must resolve the statement's accepted state/order;
1550/// witness signature alone cannot establish that user authority or disclosure.
1551pub fn verify_publication(
1552    operation: &SignedDelegatedImportOperationV1,
1553    delegation: &VerifiedImportDelegation,
1554    manifest: &ImportResultManifestV1,
1555    statement: &crate::heddle::api::common::SignedHostedWitnessStatementV1,
1556    set: &crate::witness_trust::VerifiedWitnessSet,
1557    proof: Option<&crate::heddle::api::common::HostedWitnessHistoryProofV1>,
1558    now_ms: i64,
1559) -> Result<crate::witness_trust::ResolvedWitnessStatement, Reject> {
1560    validate_statement_boundary(statement.body.as_ref().ok_or(Reject::Canonical)?)?;
1561    verify_operation(operation, delegation)?;
1562    if !check_slot_replay(manifest, operation)? {
1563        return Err(Reject::Scope);
1564    }
1565    let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
1566    let d = &delegation.body;
1567    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
1568    let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
1569    let payload = ImportPublicationWitnessV1 {
1570        format_version: 1,
1571        signed_operation_digest: signed_operation_digest(operation)?,
1572        delegation_digest: delegation.digest.clone(),
1573        logical_job_id: o.logical_job_id.clone(),
1574        retry_lineage_id: o.retry_lineage_id.clone(),
1575        physical_operation_id: o.physical_operation_id.clone(),
1576        ref_name: o.ref_name.clone(),
1577        slot_id: o.slot_id,
1578        hash_algorithm: o.hash_algorithm,
1579        observed_commit_oid: o.observed_commit_oid.clone(),
1580        expected_frontier_digest: o.expected_frontier_digest.clone(),
1581        resulting_frontier_digest: o.resulting_frontier_digest.clone(),
1582        terminal_manifest_digest: manifest_digest(manifest)?,
1583    };
1584    if s.purpose != 3
1585        || s.spool_uuid != id.spool_uuid
1586        || s.spool_genesis_digest != id.spool_genesis_digest
1587        || s.owner_id != id.owner_id
1588        || s.owner_state_hash != id.owner_state_hash
1589        || s.ownership_transfer_sequence != id.ownership_transfer_sequence
1590        || s.authority_digest != delegation.digest
1591        || s.original_signatures_digest
1592            != hash(&[&operation
1593                .job_signature
1594                .as_ref()
1595                .ok_or(Reject::Signature)?
1596                .signature])
1597        || s.canonical_payload != canonical(&payload)?
1598        || s.basis != 1
1599    {
1600        return Err(Reject::Scope);
1601    }
1602    interval(
1603        d.not_before_unix_seconds,
1604        d.expires_at_unix_seconds,
1605        s.observed_at_unix_millis / 1000,
1606    )?;
1607    crate::witness_trust::resolve_statement(set, statement, proof, false, now_ms)
1608}
1609pub fn require_hybrid_peer(
1610    protocol: Option<&crate::heddle::api::common::ProtocolCompatibility>,
1611) -> Result<(), Reject> {
1612    let protocol = protocol.ok_or(Reject::Protocol)?;
1613    if protocol.protocol_version != 2 || protocol.mandatory_features != [1] {
1614        return Err(Reject::Protocol);
1615    }
1616    Ok(())
1617}
1618
1619/// Producer-owned logical-job/lease fence for RetryImportSource and final
1620/// publication. The physical retry row never supplies a new logical identity.
1621pub fn check_job_fence(
1622    logical_job_id: &[u8],
1623    active_delegation_digest: &[u8],
1624    expected_epoch: u64,
1625    active: &VerifiedImportDelegation,
1626    durable_epoch: u64,
1627) -> Result<(), Reject> {
1628    if logical_job_id != active.body.logical_job_id {
1629        return Err(Reject::Scope);
1630    }
1631    if expected_epoch != durable_epoch || active_delegation_digest != active.digest {
1632        return Err(Reject::StaleContext);
1633    }
1634    Ok(())
1635}
1636pub fn validate_public_bundle(bundle: &ImportPublicProofBundleV1) -> Result<(), Reject> {
1637    validate_bundle_bounds(bundle)?;
1638    validate_bundle_history(bundle, true)
1639}
1640fn validate_bundle_bounds(bundle: &ImportPublicProofBundleV1) -> Result<(), Reject> {
1641    use prost::Message;
1642    if bundle.format_version != 1 {
1643        return Err(Reject::Version);
1644    }
1645    if bundle.encoded_len() > MAX_BUNDLE_BYTES
1646        || bundle.owner_histories.len() > 64
1647        || bundle.ownership_transfers.len() > 64
1648        || bundle.genesis_authorities.len() > MAX_BRANCHES
1649        || bundle.delegations.len() > 64
1650        || bundle.renewals.len() > 63
1651        || bundle.operations.len() > MAX_BRANCHES
1652        || bundle.statements.len() > 1024
1653        || bundle.history_proofs.len() > 1024
1654        || bundle.policies.len() > 256
1655        || bundle.original_geneses.len() > MAX_BRANCHES
1656        || bundle.creator_authority_envelopes.len() > MAX_BRANCHES
1657        || bundle.member_permissions.len() > 64
1658        || bundle.manifests.len() > 320
1659        || bundle.genesis_witnesses.len() > 256
1660        || bundle.authority_witnesses.len() > 256
1661        || bundle.landing_witnesses.len() > 256
1662    {
1663        return Err(Reject::Bounds);
1664    }
1665    Ok(())
1666}
1667
1668/// Typed adapter boundary: an authentic unrelated capability/online role must
1669/// never be selected as the parent of an import certificate.
1670pub enum ImportPermissionEvidence<'a> {
1671    Import(&'a SignedImportMemberPermissionV1),
1672    OwnerCapability(&'a SignedOwnerCapability),
1673    OnlineRole(&'a str),
1674}
1675pub fn select_import_permission(
1676    evidence: ImportPermissionEvidence<'_>,
1677) -> Result<&SignedImportMemberPermissionV1, Reject> {
1678    match evidence {
1679        ImportPermissionEvidence::Import(p) => Ok(p),
1680        ImportPermissionEvidence::OwnerCapability(_) | ImportPermissionEvidence::OnlineRole(_) => {
1681            Err(Reject::ImportPermission)
1682        }
1683    }
1684}
1685/// Hybrid dispatch has no legacy execution arm, even for an authentic witness.
1686pub fn require_import_operation_format(format: &str) -> Result<(), Reject> {
1687    if format != OPERATION_DOMAIN {
1688        return Err(Reject::Protocol);
1689    }
1690    Ok(())
1691}
1692pub fn frontier_digest(frontier: &ImportFrontierV1) -> Result<Vec<u8>, Reject> {
1693    if frontier.format_version != 1 {
1694        return Err(Reject::Version);
1695    }
1696    width(&frontier.thread_id, 32)?;
1697    if frontier.operation_ids.len() > 128 {
1698        return Err(Reject::Bounds);
1699    }
1700    for id in &frontier.operation_ids {
1701        width(id, 32)?;
1702    }
1703    if frontier.operation_ids.windows(2).any(|w| w[0] >= w[1]) {
1704        return Err(Reject::Canonical);
1705    }
1706    signing_digest("heddle-import-frontier-v1", frontier)
1707}
1708pub fn content_digest(content: &ImportContentV1) -> Result<Vec<u8>, Reject> {
1709    if content.format_version != 1 {
1710        return Err(Reject::Version);
1711    }
1712    if content.canonical_capture.is_empty()
1713        || content.canonical_capture.len() > MAX_RESULT_BYTES as usize
1714    {
1715        return Err(Reject::Bounds);
1716    }
1717    signing_digest("heddle-import-content-v1", content)
1718}
1719pub fn signed_native_digest(record: &SignedRecord) -> Result<Vec<u8>, Reject> {
1720    signing_digest("heddle-signed-native-record-v1", record)
1721}
1722fn verify_native(record: &SignedRecord, format: &str) -> Result<(), Reject> {
1723    if record.format != format {
1724        return Err(Reject::Version);
1725    }
1726    if record.canonical_record.is_empty()
1727        || record.canonical_record.len() > MAX_RECORD_BYTES
1728        || record.signatures.is_empty()
1729        || record.signatures.len() > 16
1730    {
1731        return Err(Reject::Bounds);
1732    }
1733    let mut previous: Option<&[u8]> = None;
1734    let input = [format.as_bytes(), b"\0", &record.canonical_record].concat();
1735    for s in &record.signatures {
1736        if previous.is_some_and(|p| p >= s.public_key.as_slice()) {
1737            return Err(Reject::Canonical);
1738        }
1739        verify(&s.public_key, &input, &s.signature)?;
1740        previous = Some(&s.public_key);
1741    }
1742    Ok(())
1743}
1744/// Recompute transport commitments from exact native evidence. The caller's
1745/// native verifier additionally authenticates manifest membership, receipt
1746/// subjects/bases, accepting authority and canonical native encoding.
1747pub fn verify_boundary_acceptance(e: &ImportBoundaryAcceptanceV1) -> Result<(), Reject> {
1748    let binding = e.binding.as_ref().ok_or(Reject::BoundaryAcceptance)?;
1749    validate_boundary_binding(binding)?;
1750    let acceptance = e
1751        .signed_acceptance
1752        .as_ref()
1753        .ok_or(Reject::BoundaryAcceptance)?;
1754    verify_native(acceptance, "heddle-original-boundary-acceptance-v1")?;
1755    if acceptance.signatures.len() != 1 {
1756        return Err(Reject::Signature);
1757    }
1758    if e.originals_manifest.is_empty()
1759        || e.publication_intent.is_empty()
1760        || e.originals_manifest.len() > MAX_RECORD_BYTES
1761        || e.publication_intent.len() > MAX_RECORD_BYTES
1762        || e.original_receipts.is_empty()
1763        || e.original_receipts.len() > 128
1764    {
1765        return Err(Reject::Bounds);
1766    }
1767    if binding.acceptance_id != native_id(acceptance)
1768        || binding.signed_acceptance_digest != signed_native_digest(acceptance)?
1769        || binding.originals_manifest_digest
1770            != boundary_octets_digest(
1771                "heddle-boundary-originals-manifest-v1",
1772                &e.originals_manifest,
1773            )
1774        || binding.publication_intent_digest
1775            != boundary_octets_digest(
1776                "heddle-boundary-publication-intent-v1",
1777                &e.publication_intent,
1778            )
1779    {
1780        return Err(Reject::BoundaryAcceptance);
1781    }
1782    let native: NativeBoundarySelection =
1783        rmp_serde::from_slice(&acceptance.canonical_record).map_err(|_| Reject::Canonical)?;
1784    if native.originals_manifest.as_slice()
1785        != native_octets_id(
1786            "heddle-original-publication-manifest-v1",
1787            &e.originals_manifest,
1788        )
1789        || native.publication_intent.as_slice()
1790            != native_octets_id(
1791                "heddle-original-publication-intent-v1",
1792                &e.publication_intent,
1793            )
1794    {
1795        return Err(Reject::BoundaryAcceptance);
1796    }
1797    let mut digests = Vec::new();
1798    for receipt in &e.original_receipts {
1799        if ![
1800            "heddle-thread-genesis-admission-v2",
1801            "heddle-thread-authority-admission-v3",
1802        ]
1803        .contains(&receipt.format.as_str())
1804        {
1805            return Err(Reject::Version);
1806        }
1807        verify_native(receipt, &receipt.format)?;
1808        if receipt.signatures.len() != 1 {
1809            return Err(Reject::Signature);
1810        }
1811        let native: NativeBoundaryReceipt =
1812            rmp_serde::from_slice(&receipt.canonical_record).map_err(|_| Reject::Canonical)?;
1813        if native.basis
1814            != (NativeBoundaryBasis::BoundaryAcceptance {
1815                acceptance: binding
1816                    .acceptance_id
1817                    .as_slice()
1818                    .try_into()
1819                    .map_err(|_| Reject::Canonical)?,
1820            })
1821        {
1822            return Err(Reject::BoundaryAcceptance);
1823        }
1824        digests.push(signed_native_digest(receipt)?);
1825    }
1826    if digests != binding.original_receipt_digests {
1827        return Err(Reject::BoundaryAcceptance);
1828    }
1829    Ok(())
1830}
1831// These readers extract only the native commitment selectors. Full native
1832// canonicality, model validity, membership and authority remain the native gate.
1833#[derive(serde::Deserialize)]
1834struct NativeBoundarySelection {
1835    originals_manifest: [u8; 32],
1836    publication_intent: [u8; 32],
1837}
1838#[derive(serde::Deserialize, PartialEq)]
1839enum NativeBoundaryBasis {
1840    OriginalAuthority,
1841    BoundaryAcceptance { acceptance: [u8; 32] },
1842}
1843#[derive(serde::Deserialize)]
1844struct NativeBoundaryReceipt {
1845    basis: NativeBoundaryBasis,
1846    thread: [u8; 32],
1847    subject: Option<NativeBoundarySubject>,
1848}
1849#[derive(serde::Deserialize)]
1850enum NativeBoundarySubject {
1851    Operation([u8; 32]),
1852    OwnershipClaim([u8; 32]),
1853    OwnershipResolution([u8; 32]),
1854}
1855fn native_octets_id(format: &str, bytes: &[u8]) -> Vec<u8> {
1856    let mut h = blake3::Hasher::new();
1857    h.update(format.as_bytes());
1858    h.update(&(bytes.len() as u64).to_le_bytes());
1859    h.update(b"\0");
1860    h.update(bytes);
1861    h.finalize().as_bytes().to_vec()
1862}
1863fn boundary_original(
1864    e: &ImportBoundaryAcceptanceV1,
1865    original: &SignedRecord,
1866) -> Result<(), Reject> {
1867    let id = native_id(original);
1868    for receipt in &e.original_receipts {
1869        let value: NativeBoundaryReceipt =
1870            rmp_serde::from_slice(&receipt.canonical_record).map_err(|_| Reject::Canonical)?;
1871        let (format, subject) = match value.subject {
1872            None if receipt.format == "heddle-thread-genesis-admission-v2" => {
1873                ("heddle-thread-genesis-v1", value.thread)
1874            }
1875            Some(NativeBoundarySubject::Operation(id)) => ("heddle-thread-operation-v1", id),
1876            Some(NativeBoundarySubject::OwnershipClaim(id)) => {
1877                ("heddle-thread-ownership-claim-v1", id)
1878            }
1879            Some(NativeBoundarySubject::OwnershipResolution(id)) => {
1880                ("heddle-thread-ownership-resolution-v1", id)
1881            }
1882            _ => return Err(Reject::BoundaryAcceptance),
1883        };
1884        if original.format == format && id == subject {
1885            return Ok(());
1886        }
1887    }
1888    Err(Reject::BoundaryAcceptance)
1889}
1890pub fn boundary_octets_digest(domain: &str, bytes: &[u8]) -> Vec<u8> {
1891    hash(&[
1892        domain.as_bytes(),
1893        &(bytes.len() as u32).to_be_bytes(),
1894        bytes,
1895    ])
1896}
1897pub fn validate_boundary_binding(
1898    b: &crate::heddle::api::common::HostedWitnessBoundaryAcceptanceV1,
1899) -> Result<(), Reject> {
1900    if b.format_version != 1 {
1901        return Err(Reject::Version);
1902    }
1903    for digest in [
1904        &b.acceptance_id,
1905        &b.signed_acceptance_digest,
1906        &b.originals_manifest_digest,
1907        &b.publication_intent_digest,
1908    ] {
1909        width(digest, 32)?;
1910    }
1911    if b.original_receipt_digests.is_empty() || b.original_receipt_digests.len() > 128 {
1912        return Err(Reject::Bounds);
1913    }
1914    for digest in &b.original_receipt_digests {
1915        width(digest, 32)?;
1916    }
1917    if b.original_receipt_digests.windows(2).any(|w| w[0] >= w[1]) {
1918        return Err(Reject::Canonical);
1919    }
1920    Ok(())
1921}
1922pub fn validate_statement_boundary(
1923    s: &crate::heddle::api::common::HostedWitnessStatementV1,
1924) -> Result<(), Reject> {
1925    match (s.basis, s.boundary_acceptance.as_ref()) {
1926        (1, None) => Ok(()),
1927        (2, Some(b)) if s.purpose == 1 || s.purpose == 2 => validate_boundary_binding(b),
1928        _ => Err(Reject::BoundaryAcceptance),
1929    }
1930}
1931fn match_boundary(
1932    s: &crate::heddle::api::common::HostedWitnessStatementV1,
1933    evidence: &[ImportBoundaryAcceptanceV1],
1934) -> Result<(), Reject> {
1935    validate_statement_boundary(s)?;
1936    let mut previous = None;
1937    for e in evidence {
1938        verify_boundary_acceptance(e)?;
1939        let b = e.binding.as_ref().ok_or(Reject::BoundaryAcceptance)?;
1940        if previous.is_some_and(|p: &[u8]| p >= b.acceptance_id.as_slice()) {
1941            return Err(Reject::Canonical);
1942        }
1943        previous = Some(b.acceptance_id.as_slice());
1944    }
1945    if let Some(binding) = &s.boundary_acceptance
1946        && !evidence.iter().any(|e| e.binding.as_ref() == Some(binding))
1947    {
1948        return Err(Reject::BoundaryAcceptance);
1949    }
1950    Ok(())
1951}
1952fn native_dependencies(
1953    records: &[SignedRecord],
1954    evidence: &[ImportBoundaryAcceptanceV1],
1955) -> Result<(), Reject> {
1956    if records.len() > 128 {
1957        return Err(Reject::Bounds);
1958    }
1959    let mut previous = None;
1960    for record in records {
1961        match record.format.as_str() {
1962            "heddle-thread-genesis-v1"
1963            | "heddle-thread-operation-v1"
1964            | "heddle-thread-ownership-claim-v1"
1965            | "heddle-thread-ownership-resolution-v1" => (),
1966            "heddle-original-boundary-acceptance-v1"
1967            | "heddle-thread-genesis-admission-v2"
1968            | "heddle-thread-authority-admission-v3" => {
1969                if !evidence.iter().any(|e| {
1970                    e.signed_acceptance.as_ref() == Some(record)
1971                        || e.original_receipts.contains(record)
1972                }) {
1973                    return Err(Reject::BoundaryAcceptance);
1974                }
1975            }
1976            _ => return Err(Reject::Version),
1977        }
1978        verify_native(record, &record.format)?;
1979        let digest = signed_native_digest(record)?;
1980        if previous.as_ref().is_some_and(|p| p >= &digest) {
1981            return Err(Reject::Canonical);
1982        }
1983        previous = Some(digest);
1984    }
1985    Ok(())
1986}
1987fn original_signatures(
1988    records: &[&SignedRecord],
1989    extra: &[RecordSignature],
1990) -> Result<Vec<u8>, Reject> {
1991    let signatures = records
1992        .iter()
1993        .flat_map(|r| r.signatures.iter())
1994        .chain(extra.iter())
1995        .collect::<Vec<_>>();
1996    let mut out = (signatures.len() as u32).to_be_bytes().to_vec();
1997    for s in signatures {
1998        s.write(&mut out)?;
1999    }
2000    Ok(hash(&[b"heddle-hosted-original-signatures-v1", &out]))
2001}
2002use crate::hybrid_codec::Canonical;
2003/// Caller constructs this from independently verified native originals and
2004/// accepted owner/policy/landing context. This matching layer verifies original
2005/// signatures and exact payload commitments separately from witness trust; it
2006/// does not replace native causal, authority or landing-model verification.
2007pub enum WitnessPayload<'a> {
2008    Genesis(&'a ImportGenesisWitnessV1),
2009    Authority(&'a ImportAuthorityWitnessV1),
2010    Landing(&'a HostedLandingWitnessV1),
2011}
2012pub fn verify_witness_payload(
2013    statement: &crate::heddle::api::common::HostedWitnessStatementV1,
2014    payload: WitnessPayload<'_>,
2015) -> Result<(), Reject> {
2016    let (purpose, bytes, authority, signatures, publisher) = match payload {
2017        WitnessPayload::Genesis(p) => {
2018            if p.format_version != 1 {
2019                return Err(Reject::Version);
2020            }
2021            let original = p.original_genesis.as_ref().ok_or(Reject::Canonical)?;
2022            let binding = p.binding.as_ref().ok_or(Reject::Canonical)?;
2023            let b = binding.body.as_ref().ok_or(Reject::Canonical)?;
2024            match_boundary(
2025                statement,
2026                &p.boundary_acceptance.iter().cloned().collect::<Vec<_>>(),
2027            )?;
2028            if let Some(e) = &p.boundary_acceptance {
2029                boundary_original(e, original)?;
2030            }
2031            if (statement.basis == 2) != p.boundary_acceptance.is_some() {
2032                return Err(Reject::BoundaryAcceptance);
2033            }
2034            verify_native(original, "heddle-thread-genesis-v1")?;
2035            if native_id(original) != b.genesis_digest {
2036                return Err(Reject::Scope);
2037            }
2038            if let Some(id) = &b.identity {
2039                if statement.spool_uuid != id.spool_uuid
2040                    || statement.spool_genesis_digest != id.spool_genesis_digest
2041                    || statement.owner_id != id.owner_id
2042                    || statement.owner_state_hash != id.owner_state_hash
2043                    || statement.ownership_transfer_sequence != id.ownership_transfer_sequence
2044                {
2045                    return Err(Reject::Scope);
2046                }
2047            } else {
2048                return Err(Reject::Canonical);
2049            }
2050            let creator = original
2051                .signatures
2052                .iter()
2053                .find(|s| s.public_key == b.creator_public_key)
2054                .ok_or(Reject::Signature)?;
2055            if b.original_creator_signature != creator.signature
2056                || b.creator_authority_envelope_digest != hash(&[&p.creator_authority_envelope])
2057            {
2058                return Err(Reject::Scope);
2059            }
2060            verify_authorization_signature(
2061                &b.creator_public_key,
2062                GENESIS_DOMAIN,
2063                b,
2064                binding
2065                    .creator_signature
2066                    .as_ref()
2067                    .ok_or(Reject::Signature)?,
2068            )?;
2069            (
2070                1,
2071                canonical(p)?,
2072                signed_genesis_digest(binding)?,
2073                original_signatures(&[original], &[])?,
2074                key_id(&b.creator_public_key),
2075            )
2076        }
2077        WitnessPayload::Authority(p) => {
2078            if p.format_version != 1 {
2079                return Err(Reject::Version);
2080            }
2081            let original = p.original.as_ref().ok_or(Reject::Canonical)?;
2082            let format = match p.kind {
2083                1 => "heddle-thread-operation-v1",
2084                2 => "heddle-thread-ownership-claim-v1",
2085                3 => "heddle-thread-ownership-resolution-v1",
2086                _ => return Err(Reject::Version),
2087            };
2088            verify_native(original, format)?;
2089            if (p.kind == 2 || p.kind == 3) && original.signatures.len() != 2 {
2090                return Err(Reject::Signature);
2091            }
2092            if !original
2093                .signatures
2094                .iter()
2095                .any(|s| key_id(&s.public_key) == statement.publisher_key_id)
2096            {
2097                return Err(Reject::Signature);
2098            }
2099            if p.authority_envelope.is_empty() || p.authority_envelope.len() > MAX_RECORD_BYTES {
2100                return Err(Reject::Bounds);
2101            }
2102            match_boundary(statement, &p.boundary_acceptances)?;
2103            if let Some(b) = &statement.boundary_acceptance {
2104                let e = p
2105                    .boundary_acceptances
2106                    .iter()
2107                    .find(|e| e.binding.as_ref() == Some(b))
2108                    .ok_or(Reject::BoundaryAcceptance)?;
2109                boundary_original(e, original)?;
2110            }
2111            native_dependencies(&p.dependencies, &p.boundary_acceptances)?;
2112            let records = std::iter::once(original)
2113                .chain(p.dependencies.iter())
2114                .collect::<Vec<_>>();
2115            (
2116                2,
2117                canonical(p)?,
2118                hash(&[
2119                    b"heddle-hosted-authority-envelope-v1",
2120                    &(p.authority_envelope.len() as u32).to_be_bytes(),
2121                    &p.authority_envelope,
2122                ]),
2123                original_signatures(&records, &[])?,
2124                statement.publisher_key_id.clone(),
2125            )
2126        }
2127        WitnessPayload::Landing(p) => {
2128            if p.format_version != 1 {
2129                return Err(Reject::Version);
2130            }
2131            let execution = p.execution.as_ref().ok_or(Reject::Canonical)?;
2132            let source = p.source_operation.as_ref().ok_or(Reject::Canonical)?;
2133            let request = p.request.as_ref().ok_or(Reject::Canonical)?;
2134            if request.format_version != 1
2135                || request.method_path != "/heddle.api.v1alpha2.ThreadService/LandThread"
2136            {
2137                return Err(Reject::Version);
2138            }
2139            verify_native(execution, "heddle-thread-operation-v1")?;
2140            verify_native(source, "heddle-thread-operation-v1")?;
2141            match_boundary(statement, &[])?;
2142            native_dependencies(&p.review_evidence, &[])?;
2143            let signature = request.signature.as_ref().ok_or(Reject::Signature)?;
2144            if request.signing_identity
2145                != format!(
2146                    "principal:device-key:{}",
2147                    hex::encode(&signature.public_key)
2148                )
2149            {
2150                return Err(Reject::Signature);
2151            }
2152            width(&request.nonce, 16)?;
2153            if request.timestamp_millis <= 0
2154                || request.request_body.is_empty()
2155                || request.request_body.len() > MAX_RECORD_BYTES
2156                || p.authority_envelope.is_empty()
2157                || p.authority_envelope.len() > MAX_RECORD_BYTES
2158            {
2159                return Err(Reject::Bounds);
2160            }
2161            let input = crate::signing::unary_bytes(
2162                &request.signing_identity,
2163                &request.method_path,
2164                request.timestamp_millis,
2165                &request.nonce,
2166                &request.request_body,
2167            );
2168            verify(&signature.public_key, &input, &signature.signature)?;
2169            let records = [execution, source]
2170                .into_iter()
2171                .chain(p.review_evidence.iter())
2172                .collect::<Vec<_>>();
2173            (
2174                4,
2175                canonical(p)?,
2176                hash(&[
2177                    b"heddle-hosted-authority-envelope-v1",
2178                    &(p.authority_envelope.len() as u32).to_be_bytes(),
2179                    &p.authority_envelope,
2180                ]),
2181                original_signatures(&records, std::slice::from_ref(signature))?,
2182                key_id(&signature.public_key),
2183            )
2184        }
2185    };
2186    if bytes.len() > MAX_RECORD_BYTES {
2187        return Err(Reject::Bounds);
2188    }
2189    if statement.purpose != purpose
2190        || statement.canonical_payload != bytes
2191        || statement.authority_digest != authority
2192        || statement.original_signatures_digest != signatures
2193        || statement.publisher_key_id != publisher
2194    {
2195        return Err(Reject::Scope);
2196    }
2197    Ok(())
2198}
2199
2200pub fn resolve_bundle_permission<'a>(
2201    bundle: &'a ImportPublicProofBundleV1,
2202    digest: &[u8],
2203) -> Result<Option<&'a SignedImportMemberPermissionV1>, Reject> {
2204    width(digest, 32)?;
2205    if digest == [0; 32] {
2206        return Ok(None);
2207    }
2208    bundle
2209        .member_permissions
2210        .iter()
2211        .find(|p| signed_permission_digest(p).is_ok_and(|d| d == digest))
2212        .map(Some)
2213        .ok_or(Reject::ImportPermission)
2214}
2215pub fn resolve_bundle_manifest<'a>(
2216    bundle: &'a ImportPublicProofBundleV1,
2217    digest: &[u8],
2218) -> Result<&'a ImportResultManifestV1, Reject> {
2219    width(digest, 32)?;
2220    bundle
2221        .manifests
2222        .iter()
2223        .find(|m| manifest_digest(m).is_ok_and(|d| d == digest))
2224        .ok_or(Reject::StaleManifest)
2225}
2226pub fn publication_payload(
2227    operation: &SignedDelegatedImportOperationV1,
2228    manifest: &ImportResultManifestV1,
2229) -> Result<ImportPublicationWitnessV1, Reject> {
2230    let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
2231    Ok(ImportPublicationWitnessV1 {
2232        format_version: 1,
2233        signed_operation_digest: signed_operation_digest(operation)?,
2234        delegation_digest: o.delegation_digest.clone(),
2235        logical_job_id: o.logical_job_id.clone(),
2236        retry_lineage_id: o.retry_lineage_id.clone(),
2237        physical_operation_id: o.physical_operation_id.clone(),
2238        ref_name: o.ref_name.clone(),
2239        slot_id: o.slot_id,
2240        hash_algorithm: o.hash_algorithm,
2241        observed_commit_oid: o.observed_commit_oid.clone(),
2242        expected_frontier_digest: o.expected_frontier_digest.clone(),
2243        resulting_frontier_digest: o.resulting_frontier_digest.clone(),
2244        terminal_manifest_digest: manifest_digest(manifest)?,
2245    })
2246}
2247/// Completeness and digest addressing only. Trust/signature verification still
2248/// uses independently selected owner contexts at each witnessed historical time.
2249fn validate_bundle_history(
2250    bundle: &ImportPublicProofBundleV1,
2251    require_admissions: bool,
2252) -> Result<(), Reject> {
2253    fn sorted<T>(
2254        values: &[T],
2255        digest: impl Fn(&T) -> Result<Vec<u8>, Reject>,
2256    ) -> Result<(), Reject> {
2257        let mut previous = None;
2258        for value in values {
2259            let d = digest(value)?;
2260            if previous.as_ref().is_some_and(|p| p >= &d) {
2261                return Err(Reject::Canonical);
2262            }
2263            previous = Some(d);
2264        }
2265        Ok(())
2266    }
2267    for statement in &bundle.statements {
2268        let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
2269        validate_statement_boundary(s)?;
2270        require_policy_history(
2271            bundle,
2272            &s.spool_uuid,
2273            s.policy_sequence,
2274            &s.policy_state_hash,
2275        )?;
2276    }
2277    sorted(&bundle.member_permissions, signed_permission_digest)?;
2278    sorted(&bundle.manifests, manifest_digest)?;
2279    if let Some(p) = &bundle.member_permission
2280        && resolve_bundle_permission(bundle, &signed_permission_digest(p)?)? != Some(p)
2281    {
2282        return Err(Reject::ImportPermission);
2283    }
2284    let terminal = bundle.terminal_manifest.as_ref().ok_or(Reject::Canonical)?;
2285    if resolve_bundle_manifest(bundle, &manifest_digest(terminal)?)? != terminal {
2286        return Err(Reject::Canonical);
2287    }
2288    if bundle.delegations.is_empty() || bundle.renewals.len() + 1 != bundle.delegations.len() {
2289        return Err(Reject::Canonical);
2290    }
2291    for (i, d) in bundle.delegations.iter().enumerate() {
2292        let body = d.body.as_ref().ok_or(Reject::Canonical)?;
2293        resolve_bundle_permission(bundle, &body.parent_permission_digest)?;
2294        if i == 0 {
2295            if body.predecessor_delegation_digest != [0; 32] {
2296                return Err(Reject::RenewalFork);
2297            }
2298        } else {
2299            let r = bundle.renewals[i - 1]
2300                .body
2301                .as_ref()
2302                .ok_or(Reject::Canonical)?;
2303            if r.replacement.as_ref() != Some(d)
2304                || r.predecessor_delegation_digest
2305                    != signed_delegation_digest(&bundle.delegations[i - 1])?
2306                || body.predecessor_delegation_digest != r.predecessor_delegation_digest
2307                || r.expected_authority_epoch != i as u64
2308            {
2309                return Err(Reject::RenewalFork);
2310            }
2311            resolve_bundle_manifest(bundle, &r.committed_manifest_digest)?;
2312        }
2313        for branch in &body.branch_manifest {
2314            let g = bundle
2315                .genesis_authorities
2316                .iter()
2317                .find(|g| {
2318                    signed_genesis_digest(g).is_ok_and(|h| h == branch.genesis_authority_digest)
2319                })
2320                .ok_or(Reject::Scope)?;
2321            let b = g.body.as_ref().ok_or(Reject::Canonical)?;
2322            resolve_bundle_permission(bundle, &b.parent_permission_digest)?;
2323            if !bundle
2324                .original_geneses
2325                .iter()
2326                .any(|o| native_id(o) == b.genesis_digest)
2327                || !bundle
2328                    .creator_authority_envelopes
2329                    .iter()
2330                    .any(|e| hash(&[e]) == b.creator_authority_envelope_digest)
2331            {
2332                return Err(Reject::Scope);
2333            }
2334            // Every branch needs its original admission, not merely its
2335            // binding. Proof-only retirement lookup cannot recover a payload.
2336            if require_admissions
2337                && !bundle.genesis_witnesses.iter().any(|payload| {
2338                    payload.binding.as_ref() == Some(g)
2339                        && payload.original_genesis.as_ref().is_some_and(|o| {
2340                            native_id(o) == b.genesis_digest && bundle.original_geneses.contains(o)
2341                        })
2342                        && hash(&[&payload.creator_authority_envelope])
2343                            == b.creator_authority_envelope_digest
2344                        && canonical(payload).is_ok_and(|bytes| {
2345                            bundle.statements.iter().any(|s| {
2346                                s.body
2347                                    .as_ref()
2348                                    .is_some_and(|s| s.purpose == 1 && s.canonical_payload == bytes)
2349                            })
2350                        })
2351                })
2352            {
2353                return Err(Reject::Scope);
2354            }
2355        }
2356    }
2357    for manifest in &bundle.manifests {
2358        validate_manifest(manifest)?;
2359        if manifest.logical_job_id != terminal.logical_job_id
2360            || manifest.retry_lineage_id != terminal.retry_lineage_id
2361        {
2362            return Err(Reject::Scope);
2363        }
2364        for slot in &manifest.slots {
2365            let operation = bundle
2366                .operations
2367                .iter()
2368                .find(|o| {
2369                    signed_operation_digest(o).is_ok_and(|d| d == slot.signed_operation_digest)
2370                })
2371                .ok_or(Reject::Scope)?;
2372            if !check_slot_replay(manifest, operation)? || !check_slot_replay(terminal, operation)?
2373            {
2374                return Err(Reject::Scope);
2375            }
2376        }
2377    }
2378    for operation in &bundle.operations {
2379        let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
2380        if !bundle
2381            .delegations
2382            .iter()
2383            .any(|d| signed_delegation_digest(d).is_ok_and(|h| h == o.delegation_digest))
2384            || !check_slot_replay(terminal, operation)?
2385        {
2386            return Err(Reject::Scope);
2387        }
2388        if !bundle.manifests.iter().any(|m| {
2389            check_slot_replay(m, operation) == Ok(true)
2390                && publication_payload(operation, m)
2391                    .and_then(|p| canonical(&p))
2392                    .is_ok_and(|p| {
2393                        bundle.statements.iter().any(|s| {
2394                            s.body
2395                                .as_ref()
2396                                .is_some_and(|s| s.purpose == 3 && s.canonical_payload == p)
2397                        })
2398                    })
2399        }) {
2400            return Err(Reject::Scope);
2401        }
2402    }
2403    for statement in &bundle.statements {
2404        let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
2405        let found = match s.purpose {
2406            1 => bundle
2407                .genesis_witnesses
2408                .iter()
2409                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
2410            2 => bundle
2411                .authority_witnesses
2412                .iter()
2413                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
2414            3 => bundle.operations.iter().any(|o| {
2415                bundle.manifests.iter().any(|m| {
2416                    publication_payload(o, m)
2417                        .and_then(|p| canonical(&p))
2418                        .is_ok_and(|p| p == s.canonical_payload)
2419                })
2420            }),
2421            4 => bundle
2422                .landing_witnesses
2423                .iter()
2424                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
2425            _ => return Err(Reject::Version),
2426        };
2427        if !found {
2428            return Err(Reject::Scope);
2429        }
2430    }
2431    Ok(())
2432}
2433/// Reference completeness only. Native verification must authenticate every
2434/// selected policy, its owner context and the receipt before using its time.
2435fn require_policy_history(
2436    bundle: &ImportPublicProofBundleV1,
2437    spool: &[u8],
2438    mut sequence: u64,
2439    state_hash: &[u8],
2440) -> Result<(), Reject> {
2441    let mut state_hash = state_hash.to_vec();
2442    for _ in 0..=bundle.policies.len() {
2443        width(&state_hash, 32)?;
2444        if sequence == 0 {
2445            return if state_hash == [0; 32] {
2446                Ok(())
2447            } else {
2448                Err(Reject::Scope)
2449            };
2450        }
2451        let mut matches = bundle
2452            .policies
2453            .iter()
2454            .filter_map(|p| p.body.as_ref())
2455            .filter(|p| {
2456                p.spool_uuid == spool && p.sequence == sequence && p.policy_state_hash == state_hash
2457            });
2458        let policy = matches.next().ok_or(Reject::Scope)?;
2459        if matches.next().is_some() {
2460            return Err(Reject::Canonical);
2461        }
2462        let head = policy.expected_head.as_ref().ok_or(Reject::Canonical)?;
2463        if head.sequence.checked_add(1) != Some(sequence) {
2464            return Err(Reject::Scope);
2465        }
2466        sequence = head.sequence;
2467        state_hash = head.state_hash.clone();
2468    }
2469    Err(Reject::Scope)
2470}
2471fn native_id(record: &SignedRecord) -> Vec<u8> {
2472    let mut h = blake3::Hasher::new();
2473    h.update(record.format.as_bytes());
2474    h.update(&(record.canonical_record.len() as u64).to_le_bytes());
2475    h.update(b"\0");
2476    h.update(&record.canonical_record);
2477    h.finalize().as_bytes().to_vec()
2478}
2479/// Prepare response is a coherent proposal, never authority. Caller must compare
2480/// the IDs, predecessor and manifest before asking its device to sign renewal.
2481pub fn validate_renewal_preparation(response: &PrepareImportJobResponse) -> Result<(), Reject> {
2482    let state = response.renewal_state.as_ref().ok_or(Reject::Canonical)?;
2483    let proposal = response.proposal.as_ref().ok_or(Reject::Canonical)?;
2484    let previous = state.active_predecessor.as_ref().ok_or(Reject::Canonical)?;
2485    let p = previous.body.as_ref().ok_or(Reject::Canonical)?;
2486    let manifest = state.committed_manifest.as_ref().ok_or(Reject::Canonical)?;
2487    validate_manifest(manifest)?;
2488    if state.format_version != 1
2489        || state.authority_epoch == 0
2490        || state.logical_job_id != p.logical_job_id
2491        || state.retry_lineage_id != p.retry_lineage_id
2492        || proposal.logical_job_id != state.logical_job_id
2493        || proposal.retry_lineage_id != state.retry_lineage_id
2494        || manifest.logical_job_id != state.logical_job_id
2495        || manifest.retry_lineage_id != state.retry_lineage_id
2496        || proposal.predecessor_delegation_digest != signed_delegation_digest(previous)?
2497    {
2498        return Err(Reject::StaleContext);
2499    }
2500    Ok(())
2501}
2502
2503/// Caller-generated non-nil UUID, reserved as the first physical operation ID.
2504/// Occupancy is checked under the host's reservation/activation transaction.
2505pub fn initial_operation_id(lineage: &[u8], occupied: bool) -> Result<String, Reject> {
2506    width(lineage, 16)?;
2507    if lineage.iter().all(|b| *b == 0) {
2508        return Err(Reject::Canonical);
2509    }
2510    if occupied {
2511        return Err(Reject::OperationIdReused);
2512    }
2513    let h = hex::encode(lineage);
2514    Ok(format!(
2515        "{}-{}-{}-{}-{}",
2516        &h[..8],
2517        &h[8..12],
2518        &h[12..16],
2519        &h[16..20],
2520        &h[20..]
2521    ))
2522}
2523
2524/// Evaluate inside the cancellation transaction, after caller-scoped replay lookup.
2525/// The selector names only the ACTIVE delegation. Parent revocation is independent.
2526pub fn check_cancel_request(
2527    request: &CancelImportJobRequest,
2528    active: &SignedImportJobDelegationV1,
2529    durable_epoch: u64,
2530    cancelled: bool,
2531) -> Result<(), Reject> {
2532    let d = active.body.as_ref().ok_or(Reject::Canonical)?;
2533    width(&request.logical_job_id, 16)?;
2534    width(&request.cancellation_id, 32)?;
2535    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
2536    if request.client_operation_id.is_empty()
2537        || request.destination.as_ref().is_none_or(|s| {
2538            initial_operation_id(&id.spool_uuid, false).map_or(true, |uuid| s.id != uuid)
2539        })
2540        || request.logical_job_id != d.logical_job_id
2541    {
2542        return Err(Reject::Scope);
2543    }
2544    if durable_epoch == 0 || request.expected_authority_epoch != durable_epoch {
2545        return Err(Reject::StaleContext);
2546    }
2547    if request.cancellation_id != d.cancellation_id {
2548        return Err(Reject::Scope);
2549    }
2550    if cancelled {
2551        return Err(Reject::Revoked);
2552    }
2553    Ok(())
2554}
2555/// Exact replay acknowledges the stored cancellation without advancing the epoch.
2556pub fn check_cancel_replay(
2557    request: &CancelImportJobRequest,
2558    stored: &CancelImportJobRequest,
2559) -> Result<(), Reject> {
2560    if request != stored {
2561        return Err(Reject::OperationIdReused);
2562    }
2563    Ok(())
2564}
2565/// Check both independent revocation selectors, regardless of Cancel's selector.
2566pub fn check_import_revocations(
2567    delegation: &SignedImportJobDelegationV1,
2568    member: Option<&SignedImportMemberPermissionV1>,
2569    revoked: &[Vec<u8>],
2570) -> Result<(), Reject> {
2571    let d = delegation.body.as_ref().ok_or(Reject::Canonical)?;
2572    width(&d.cancellation_id, 32)?;
2573    if revoked.contains(&d.cancellation_id) {
2574        return Err(Reject::Revoked);
2575    }
2576    if let Some(parent) = member {
2577        let p = parent.body.as_ref().ok_or(Reject::ImportPermission)?;
2578        width(&p.cancellation_id, 32)?;
2579        if revoked.contains(&p.cancellation_id) {
2580            return Err(Reject::Revoked);
2581        }
2582    }
2583    Ok(())
2584}
2585
2586fn remaining_scope(
2587    scope: &ImportPermissionScopeV1,
2588    old: &ImportPermissionScopeV1,
2589    committed: &ImportResultManifestV1,
2590) -> Result<(), Reject> {
2591    if !scope_subset(scope, old) {
2592        return Err(Reject::RenewalFork);
2593    }
2594    let mut consumed = 0_u64;
2595    let mut removed = 0_u32;
2596    for slot in &committed.slots {
2597        if let Some(branch) = old
2598            .branches
2599            .iter()
2600            .find(|b| b.ref_name == slot.ref_name && b.slot_id == slot.slot_id)
2601        {
2602            if slot.result_bytes > branch.max_result_bytes {
2603                return Err(Reject::RenewalFork);
2604            }
2605            consumed = consumed
2606                .checked_add(slot.result_bytes)
2607                .ok_or(Reject::Bounds)?;
2608            removed += 1;
2609        }
2610        if scope
2611            .branches
2612            .iter()
2613            .any(|b| b.ref_name == slot.ref_name && b.slot_id == slot.slot_id)
2614        {
2615            return Err(Reject::CommittedSlot);
2616        }
2617    }
2618    if scope.max_operations
2619        > old
2620            .max_operations
2621            .checked_sub(removed)
2622            .ok_or(Reject::RenewalFork)?
2623        || scope.max_result_bytes
2624            > old
2625                .max_result_bytes
2626                .checked_sub(consumed)
2627                .ok_or(Reject::RenewalFork)?
2628    {
2629        return Err(Reject::RenewalFork);
2630    }
2631    Ok(())
2632}
2633
2634/// Signature/scope-verified recovery evidence. This is neither currently
2635/// executable authority nor evidence of historical admission. No receipt needed.
2636///
2637/// ```compile_fail
2638/// use heddle_api::import_authority::{VerifiedImportRenewalPredecessor, verify_new_operation};
2639/// use heddle_api::heddle::api::v1alpha2::SignedDelegatedImportOperationV1;
2640/// fn cannot_execute(op: &SignedDelegatedImportOperationV1, old: &VerifiedImportRenewalPredecessor) {
2641///     verify_new_operation(op, old, 0);
2642/// }
2643/// ```
2644#[derive(Debug, Clone)]
2645pub struct VerifiedImportRenewalPredecessor {
2646    previous: VerifiedImportDelegation,
2647    state_digest: Vec<u8>,
2648}
2649fn validate_cas_state(state: &ImportJobCasStateV1) -> Result<(), Reject> {
2650    let previous = state.active_predecessor.as_ref().ok_or(Reject::Canonical)?;
2651    let p = previous.body.as_ref().ok_or(Reject::Canonical)?;
2652    let manifest = state.committed_manifest.as_ref().ok_or(Reject::Canonical)?;
2653    validate_manifest(manifest)?;
2654    if state.format_version != 1
2655        || state.authority_epoch == 0
2656        || state.logical_job_id != p.logical_job_id
2657        || state.retry_lineage_id != p.retry_lineage_id
2658        || manifest.logical_job_id != state.logical_job_id
2659        || manifest.retry_lineage_id != state.retry_lineage_id
2660    {
2661        return Err(Reject::StaleContext);
2662    }
2663    Ok(())
2664}
2665/// State MUST come from authenticated job-state read / Prepare or receiver-owned durable state.
2666/// Expected owner context independently verifies the predecessor's selected history.
2667pub fn verify_renewal_predecessor(
2668    state: &ImportJobCasStateV1,
2669    member: Option<&SignedImportMemberPermissionV1>,
2670    expected: &ImportOwnerExpectation<'_>,
2671) -> Result<VerifiedImportRenewalPredecessor, Reject> {
2672    validate_cas_state(state)?;
2673    let previous = verify_delegation_inner(
2674        state.active_predecessor.as_ref().ok_or(Reject::Canonical)?,
2675        member,
2676        expected,
2677        false,
2678    )?;
2679    Ok(VerifiedImportRenewalPredecessor {
2680        previous,
2681        state_digest: signing_digest("heddle-import-job-cas-state-v1", state)?,
2682    })
2683}
2684/// Current replacement authority remains mandatory. Activation MUST recheck the
2685/// durable active predecessor/epoch/manifest and terminal state atomically.
2686pub fn verify_renewal_from_state(
2687    signed: &SignedImportJobRenewalV1,
2688    previous: &VerifiedImportRenewalPredecessor,
2689    state: &ImportJobCasStateV1,
2690    member: Option<&SignedImportMemberPermissionV1>,
2691    expected: &ImportOwnerExpectation<'_>,
2692) -> Result<VerifiedImportDelegation, Reject> {
2693    validate_cas_state(state)?;
2694    if signing_digest("heddle-import-job-cas-state-v1", state)? != previous.state_digest {
2695        return Err(Reject::StaleContext);
2696    }
2697    verify_renewal(
2698        signed,
2699        &previous.previous,
2700        state.committed_manifest.as_ref().ok_or(Reject::Canonical)?,
2701        state.authority_epoch,
2702        member,
2703        expected,
2704    )
2705}
2706
2707/// Validate discovery metadata only; a well-shaped selector grants no authority.
2708pub fn validate_hybrid_import_job_selector(
2709    selector: &HybridImportJobSelector,
2710) -> Result<(), Reject> {
2711    width(&selector.logical_job_id, 16)?;
2712    if selector.logical_job_id.iter().all(|byte| *byte == 0) {
2713        return Err(Reject::Canonical);
2714    }
2715    Ok(())
2716}
2717
2718/// Project a visible operation's durable HYBRID association into the existing
2719/// writer-only state read. Missing/unknown subject or selector is unavailable.
2720/// Malformed present metadata is rejected; never substitute an attempt ID.
2721/// This validates shape, not operation visibility, writer access or signatures.
2722pub fn import_job_state_request_from_operation(
2723    operation: &OperationRecord,
2724) -> Result<Option<GetImportJobStateRequest>, Reject> {
2725    let Some(operation_subject::Subject::Import(subject)) = operation
2726        .subject
2727        .as_ref()
2728        .and_then(|subject| subject.subject.as_ref())
2729    else {
2730        return Ok(None);
2731    };
2732    let Some(selector) = subject.hybrid_job.as_ref() else {
2733        return Ok(None);
2734    };
2735    validate_hybrid_import_job_selector(selector)?;
2736    let request = GetImportJobStateRequest {
2737        destination: operation
2738            .r#ref
2739            .as_ref()
2740            .and_then(|record| record.spool.clone()),
2741        logical_job_id: selector.logical_job_id.clone(),
2742    };
2743    validate_job_state_request(&request)?;
2744    Ok(Some(request))
2745}
2746
2747/// Finite destination-writer read; transport authentication/authorization belongs
2748/// to the generated RPC contract. Validate before any storage lookup.
2749pub fn validate_job_state_request(request: &GetImportJobStateRequest) -> Result<(), Reject> {
2750    use prost::Message;
2751    if request.encoded_len() > 4096 {
2752        return Err(Reject::Bounds);
2753    }
2754    initial_operation_id(&request.logical_job_id, false)?;
2755    let destination = request.destination.as_ref().ok_or(Reject::Scope)?;
2756    let compact = destination.id.replace('-', "");
2757    let raw = hex::decode(&compact).map_err(|_| Reject::Scope)?;
2758    if initial_operation_id(&raw, false).map_or(true, |id| id != destination.id) {
2759        return Err(Reject::Scope);
2760    }
2761    Ok(())
2762}
2763
2764fn bundle_owner_reference(
2765    bundle: &ImportPublicProofBundleV1,
2766    id: &ImportIdentityV1,
2767) -> Result<(), Reject> {
2768    identity(id)?;
2769    if !bundle.owner_histories.iter().any(|h| {
2770        h.state_hash == id.owner_state_hash
2771            && h.root
2772                .as_ref()
2773                .and_then(|r| r.root.as_ref())
2774                .is_some_and(|r| {
2775                    r.owner_id == id.owner_id && r.account_uuid == id.owner_account_uuid
2776                })
2777    }) {
2778        return Err(Reject::Root);
2779    }
2780    Ok(())
2781}
2782
2783fn validate_retained_proof(
2784    state: &ImportJobCasStateV1,
2785    proof: &ImportPublicProofBundleV1,
2786) -> Result<(), Reject> {
2787    validate_cas_state(state)?;
2788    validate_bundle_bounds(proof)?;
2789    let manifest = state.committed_manifest.as_ref().ok_or(Reject::Canonical)?;
2790    if proof.delegations.last() != state.active_predecessor.as_ref()
2791        || proof.terminal_manifest.as_ref() != Some(manifest)
2792    {
2793        return Err(Reject::StaleContext);
2794    }
2795    // An empty authenticated snapshot is recovery evidence, not proof of past
2796    // admission. Nonempty snapshots retain the full public export closure.
2797    validate_bundle_history(proof, !manifest.slots.is_empty())?;
2798    let active = state
2799        .active_predecessor
2800        .as_ref()
2801        .and_then(|d| d.body.as_ref())
2802        .ok_or(Reject::Canonical)?;
2803    let id = active.identity.as_ref().ok_or(Reject::Canonical)?;
2804    let genesis = proof
2805        .owner_genesis
2806        .as_ref()
2807        .and_then(|g| g.genesis.as_ref())
2808        .ok_or(Reject::Root)?;
2809    if genesis.spool_uuid != id.spool_uuid {
2810        return Err(Reject::Root);
2811    }
2812    let chain = proof.owner_chain.as_ref().ok_or(Reject::Root)?;
2813    if owner_chain_digest(chain)? != active.owner_chain_digest
2814        || chain.spool_genesis_digest != id.spool_genesis_digest
2815    {
2816        return Err(Reject::Root);
2817    }
2818    for d in &proof.delegations {
2819        let body = d.body.as_ref().ok_or(Reject::Canonical)?;
2820        if body.logical_job_id != state.logical_job_id
2821            || body.retry_lineage_id != state.retry_lineage_id
2822        {
2823            return Err(Reject::Scope);
2824        }
2825        bundle_owner_reference(proof, body.identity.as_ref().ok_or(Reject::Canonical)?)?;
2826    }
2827    for g in &proof.genesis_authorities {
2828        bundle_owner_reference(
2829            proof,
2830            g.body
2831                .as_ref()
2832                .and_then(|g| g.identity.as_ref())
2833                .ok_or(Reject::Canonical)?,
2834        )?;
2835    }
2836    Ok(())
2837}
2838
2839/// Supply the authenticated read, never an incoming untrusted state assertion.
2840pub fn validate_job_state_response(
2841    request: &GetImportJobStateRequest,
2842    response: &GetImportJobStateResponse,
2843) -> Result<(), Reject> {
2844    use prost::Message;
2845    validate_job_state_request(request)?;
2846    if response.encoded_len() > 2 * MAX_BUNDLE_BYTES {
2847        return Err(Reject::Bounds);
2848    }
2849    let state = response.state.as_ref().ok_or(Reject::Canonical)?;
2850    let proof = response.retained_proof.as_ref().ok_or(Reject::Canonical)?;
2851    validate_retained_proof(state, proof)?;
2852    let id = state
2853        .active_predecessor
2854        .as_ref()
2855        .and_then(|d| d.body.as_ref())
2856        .and_then(|d| d.identity.as_ref())
2857        .ok_or(Reject::Canonical)?;
2858    if state.logical_job_id != request.logical_job_id
2859        || request.destination.as_ref().is_none_or(|s| {
2860            initial_operation_id(&id.spool_uuid, false).map_or(true, |uuid| s.id != uuid)
2861        })
2862    {
2863        return Err(Reject::Scope);
2864    }
2865    Ok(())
2866}
2867
2868/// A publication/renewal race requires recomputation and a new exact Prepare
2869/// before signing. Compare the complete snapshot, including signed predecessor.
2870pub fn validate_renewal_preparation_from_read(
2871    request: &PrepareImportJobRequest,
2872    response: &PrepareImportJobResponse,
2873    read: &GetImportJobStateResponse,
2874) -> Result<(), Reject> {
2875    validate_preparation_response(request, response)?;
2876    let state = read.state.as_ref().ok_or(Reject::Canonical)?;
2877    validate_job_state_response(
2878        &GetImportJobStateRequest {
2879            destination: request.destination.clone(),
2880            logical_job_id: request.renew_logical_job_id.clone(),
2881        },
2882        read,
2883    )?;
2884    if response.renewal_state.as_ref() != Some(state) {
2885        return Err(Reject::StaleContext);
2886    }
2887    Ok(())
2888}
2889
2890/// Composition/reference validation only. Independently verify owner histories,
2891/// policies, accepted admissions/publications and the current owner head before
2892/// using them. The authenticated read supplies exact retained evidence.
2893pub fn validate_renew_request(
2894    request: &RenewImportJobRequest,
2895    read: &GetImportJobStateResponse,
2896) -> Result<(), Reject> {
2897    use prost::Message;
2898    if request.encoded_len() > 2 * MAX_BUNDLE_BYTES {
2899        return Err(Reject::Bounds);
2900    }
2901    if request.client_operation_id.is_empty() || request.client_operation_id.len() > 128 {
2902        return Err(Reject::Canonical);
2903    }
2904    let signed = request.renewal.as_ref().ok_or(Reject::Canonical)?;
2905    let renewal = signed.body.as_ref().ok_or(Reject::Canonical)?;
2906    let replacement = renewal
2907        .replacement
2908        .as_ref()
2909        .and_then(|d| d.body.as_ref())
2910        .ok_or(Reject::Canonical)?;
2911    validate_job_state_response(
2912        &GetImportJobStateRequest {
2913            destination: request.destination.clone(),
2914            logical_job_id: replacement.logical_job_id.clone(),
2915        },
2916        read,
2917    )?;
2918    let state = read.state.as_ref().ok_or(Reject::Canonical)?;
2919    let retained = read.retained_proof.as_ref().ok_or(Reject::Canonical)?;
2920    let proof = request.proof.as_ref().ok_or(Reject::Canonical)?;
2921    validate_bundle_bounds(proof)?;
2922    if renewal.predecessor_delegation_digest
2923        != signed_delegation_digest(state.active_predecessor.as_ref().ok_or(Reject::Canonical)?)?
2924        || renewal.expected_authority_epoch != state.authority_epoch
2925    {
2926        return Err(Reject::StaleContext);
2927    }
2928    if renewal.committed_manifest_digest
2929        != manifest_digest(state.committed_manifest.as_ref().ok_or(Reject::Canonical)?)?
2930    {
2931        return Err(Reject::StaleManifest);
2932    }
2933    // Normalize only the four permitted additions/current selectors. Every
2934    // other retained byte (including original parents and accepted history)
2935    // must stay exact. A pending candidate can never enter accepted arrays.
2936    let parent = resolve_bundle_permission(proof, &replacement.parent_permission_digest)?;
2937    let mut normalized = proof.clone();
2938    let mut permissions = retained.member_permissions.clone();
2939    if let Some(parent) = parent
2940        && !permissions.contains(parent)
2941    {
2942        permissions.push(parent.clone());
2943    }
2944    let mut addressed = permissions
2945        .into_iter()
2946        .map(|p| Ok((signed_permission_digest(&p)?, p)))
2947        .collect::<Result<Vec<_>, Reject>>()?;
2948    addressed.sort_by(|a, b| a.0.cmp(&b.0));
2949    let permissions = addressed.into_iter().map(|(_, p)| p).collect::<Vec<_>>();
2950    if proof.member_permissions != permissions {
2951        return Err(Reject::ImportPermission);
2952    }
2953    if proof
2954        .member_permission
2955        .as_ref()
2956        .is_some_and(|p| Some(p) != parent)
2957    {
2958        return Err(Reject::ImportPermission);
2959    }
2960    if !retained
2961        .owner_histories
2962        .iter()
2963        .all(|h| proof.owner_histories.contains(h))
2964        || !proof
2965            .ownership_transfers
2966            .starts_with(&retained.ownership_transfers)
2967        || !retained.policies.iter().all(|p| proof.policies.contains(p))
2968    {
2969        return Err(Reject::Root);
2970    }
2971    let chain = proof.owner_chain.as_ref().ok_or(Reject::Root)?;
2972    let id = replacement.identity.as_ref().ok_or(Reject::Canonical)?;
2973    bundle_owner_reference(proof, id)?;
2974    if owner_chain_digest(chain)? != replacement.owner_chain_digest
2975        || chain.spool_genesis_digest != id.spool_genesis_digest
2976    {
2977        return Err(Reject::Root);
2978    }
2979    normalized.member_permissions = retained.member_permissions.clone();
2980    normalized.member_permission = retained.member_permission.clone();
2981    normalized.owner_histories = retained.owner_histories.clone();
2982    normalized.ownership_transfers = retained.ownership_transfers.clone();
2983    normalized.policies = retained.policies.clone();
2984    normalized.owner_chain = retained.owner_chain.clone();
2985    if &normalized != retained {
2986        return Err(Reject::Scope);
2987    }
2988    let initial = retained
2989        .delegations
2990        .first()
2991        .and_then(|d| d.body.as_ref())
2992        .ok_or(Reject::Canonical)?;
2993    for branch in &replacement.branch_manifest {
2994        if !initial.branch_manifest.iter().any(|b| {
2995            b.genesis_authority_digest == branch.genesis_authority_digest
2996                && b.limit
2997                    .as_ref()
2998                    .zip(branch.limit.as_ref())
2999                    .is_some_and(|(a, b)| {
3000                        a.genesis_digest == b.genesis_digest
3001                            && a.ref_name == b.ref_name
3002                            && a.slot_id == b.slot_id
3003                    })
3004        }) {
3005            return Err(Reject::GenesisBinding);
3006        }
3007    }
3008    Ok(())
3009}
3010
3011/// Full typed renewal checks with separate independently authenticated historical
3012/// predecessor and current replacement contexts. Host transaction gates remain.
3013pub fn verify_renew_submission(
3014    request: &RenewImportJobRequest,
3015    read: &GetImportJobStateResponse,
3016    predecessor_owner: &ImportOwnerExpectation<'_>,
3017    current_owner: &ImportOwnerExpectation<'_>,
3018) -> Result<VerifiedImportDelegation, Reject> {
3019    validate_renew_request(request, read)?;
3020    let state = read.state.as_ref().ok_or(Reject::Canonical)?;
3021    let retained = read.retained_proof.as_ref().ok_or(Reject::Canonical)?;
3022    let active = state
3023        .active_predecessor
3024        .as_ref()
3025        .and_then(|d| d.body.as_ref())
3026        .ok_or(Reject::Canonical)?;
3027    let old = verify_renewal_predecessor(
3028        state,
3029        resolve_bundle_permission(retained, &active.parent_permission_digest)?,
3030        predecessor_owner,
3031    )?;
3032    let renewal = request.renewal.as_ref().ok_or(Reject::Canonical)?;
3033    let replacement = renewal
3034        .body
3035        .as_ref()
3036        .and_then(|r| r.replacement.as_ref())
3037        .and_then(|d| d.body.as_ref())
3038        .ok_or(Reject::Canonical)?;
3039    verify_renewal_from_state(
3040        renewal,
3041        &old,
3042        state,
3043        resolve_bundle_permission(
3044            request.proof.as_ref().ok_or(Reject::Canonical)?,
3045            &replacement.parent_permission_digest,
3046        )?,
3047        current_owner,
3048    )
3049}
3050
3051/// Frozen protobuf request replay is distinct from HYBRID signed-body encoding.
3052/// Hosts compare retained raw bytes before CAS/expiry checks, in caller scope.
3053pub fn check_renew_replay(request_bytes: &[u8], stored_bytes: &[u8]) -> Result<(), Reject> {
3054    if request_bytes.len() > 2 * MAX_BUNDLE_BYTES {
3055        return Err(Reject::Bounds);
3056    }
3057    if request_bytes != stored_bytes {
3058        return Err(Reject::OperationIdReused);
3059    }
3060    Ok(())
3061}