lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 706f459593179d0ac1906f26eca47450d66d7e20
parent 98718659de8314eb59c77bcdb276a1d119609981
Author: triesap <tyson@radroots.org>
Date:   Wed,  5 Aug 2026 18:47:51 +0000

feat(storage): bind private artifacts to envelope context

Diffstat:
Mcrates/storage/src/error.rs | 18++++++++++++++++++
Mcrates/storage/src/memory.rs | 32++++++++++++++++++++++++++++++--
Mcrates/storage/src/private_artifact.rs | 395+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/storage/tests/private_artifact.rs | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mdocs/api/radroots_storage.txt | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 649 insertions(+), 12 deletions(-)

diff --git a/crates/storage/src/error.rs b/crates/storage/src/error.rs @@ -87,11 +87,15 @@ pub enum Error { InvalidPrivateArtifactSecretReference, InvalidPrivateArtifactRetention, InvalidPrivateArtifactRevision, + InvalidPrivateArtifactResealId, + InvalidPrivateArtifactResealRequest, InvalidPrivateArtifactTimestamp, InvalidPrivateArtifactMetadata, PrivateArtifactNotFound, PrivateArtifactConflict, PrivateArtifactRevisionConflict, + PrivateArtifactResealConflict, + PrivateArtifactPersistenceIndeterminate, PrivateArtifactRetentionActive, PrivateArtifactNotExpired, PrivateArtifactTombstoned, @@ -217,6 +221,10 @@ impl fmt::Display for Error { "storage private-artifact retention is invalid" } Self::InvalidPrivateArtifactRevision => "storage private-artifact revision is invalid", + Self::InvalidPrivateArtifactResealId => "storage private-artifact reseal id is invalid", + Self::InvalidPrivateArtifactResealRequest => { + "storage private-artifact reseal request is invalid" + } Self::InvalidPrivateArtifactTimestamp => { "storage private-artifact timestamp is invalid" } @@ -228,6 +236,12 @@ impl fmt::Display for Error { Self::PrivateArtifactRevisionConflict => { "storage private-artifact revision conflicts with durable state" } + Self::PrivateArtifactResealConflict => { + "storage private-artifact reseal conflicts with durable state" + } + Self::PrivateArtifactPersistenceIndeterminate => { + "storage private-artifact persistence outcome is indeterminate" + } Self::PrivateArtifactRetentionActive => "storage private-artifact retention is active", Self::PrivateArtifactNotExpired => "storage private artifact has not expired", Self::PrivateArtifactTombstoned => "storage private artifact is tombstoned", @@ -355,11 +369,15 @@ mod tests { InvalidPrivateArtifactSecretReference, InvalidPrivateArtifactRetention, InvalidPrivateArtifactRevision, + InvalidPrivateArtifactResealId, + InvalidPrivateArtifactResealRequest, InvalidPrivateArtifactTimestamp, InvalidPrivateArtifactMetadata, PrivateArtifactNotFound, PrivateArtifactConflict, PrivateArtifactRevisionConflict, + PrivateArtifactResealConflict, + PrivateArtifactPersistenceIndeterminate, PrivateArtifactRetentionActive, PrivateArtifactNotExpired, PrivateArtifactTombstoned, diff --git a/crates/storage/src/memory.rs b/crates/storage/src/memory.rs @@ -37,8 +37,8 @@ use crate::{ }, private_artifact::{ DeletionReason, EXPIRED_ARTIFACT_QUERY_LIMIT_MAX, PrivateArtifactId, - PrivateArtifactMetadata, PrivateArtifactRevision, PrivateArtifactStage, - PrivateArtifactStatus, PrivateArtifactStore, + PrivateArtifactMetadata, PrivateArtifactResealReceipt, PrivateArtifactResealRequest, + PrivateArtifactRevision, PrivateArtifactStage, PrivateArtifactStatus, PrivateArtifactStore, }, projection::{ EventIndexCheckpoint, EventIndexManifest, ProjectionCheckpoint, ProjectionGeneration, @@ -70,6 +70,7 @@ struct State { event_index_manifests: Vec<EventIndexManifest>, event_index_checkpoints: Vec<EventIndexCheckpoint>, private_artifacts: Vec<PrivateArtifactMetadata>, + private_artifact_reseals: Vec<PrivateArtifactResealReceipt>, backups: Vec<BackupOperation>, restores: Vec<RestoreOperation>, atomic_receipts: Vec<AtomicCommitReceipt>, @@ -100,6 +101,7 @@ impl MemoryStorage { event_index_manifests: Vec::new(), event_index_checkpoints: Vec::new(), private_artifacts: Vec::new(), + private_artifact_reseals: Vec::new(), backups: Vec::new(), restores: Vec::new(), atomic_receipts: Vec::new(), @@ -995,6 +997,32 @@ impl PrivateArtifactStore for MemoryStorage { }) } + fn reseal_metadata( + &self, + request: PrivateArtifactResealRequest, + ) -> BoxFuture<'_, Result<PrivateArtifactResealReceipt, Error>> { + Box::pin(async move { + let mut state = self.state()?; + if let Some(receipt) = state + .private_artifact_reseals + .iter() + .find(|receipt| receipt.reseal_id() == request.reseal_id()) + { + return receipt.replay(&request); + } + let metadata = state + .private_artifacts + .iter_mut() + .find(|metadata| metadata.artifact_id() == request.artifact_id()) + .ok_or(Error::PrivateArtifactNotFound)?; + let next = metadata.resealed(&request)?; + let receipt = PrivateArtifactResealReceipt::committed(&request, next.revision()); + *metadata = next; + state.private_artifact_reseals.push(receipt); + Ok(receipt) + }) + } + fn mark_expired( &self, artifact_id: PrivateArtifactId, diff --git a/crates/storage/src/private_artifact.rs b/crates/storage/src/private_artifact.rs @@ -5,17 +5,22 @@ use core::fmt; use radroots_transport::BoxFuture; +use sha2::{Digest, Sha256}; use crate::Error; -pub const ARTIFACT_KIND_MAX_BYTES: usize = 128; +pub const ARTIFACT_KIND_MAX_BYTES: usize = 96; pub const ARTIFACT_SCHEMA_MAX_BYTES: usize = 128; pub const SECRET_PROVIDER_MAX_BYTES: usize = 64; pub const SECRET_REFERENCE_MAX_BYTES: usize = 512; pub const EXPIRED_ARTIFACT_QUERY_LIMIT_MAX: u16 = 256; +pub const PRIVATE_ARTIFACT_ENVELOPE_PURPOSE_PREFIX: &str = "radroots.private_artifact."; +pub const PRIVATE_ARTIFACT_ENVELOPE_SUBJECT_TYPE: &str = "private_artifact"; +const ENVELOPE_CONTEXT_DOMAIN: &[u8] = b"radroots.envelope_context.v1"; +const ENVELOPE_CONTEXT_VERSION: u16 = 1; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct PrivateArtifactId([u8; 16]); impl PrivateArtifactId { @@ -30,6 +35,12 @@ impl PrivateArtifactId { } } +impl fmt::Debug for PrivateArtifactId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("PrivateArtifactId(<redacted>)") + } +} + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct ArtifactKind(String); @@ -37,7 +48,7 @@ pub struct ArtifactKind(String); impl ArtifactKind { pub fn parse(value: impl Into<String>) -> Result<Self, Error> { let value = value.into(); - if !valid_label(value.as_str(), ARTIFACT_KIND_MAX_BYTES) { + if !valid_namespaced(value.as_str(), ARTIFACT_KIND_MAX_BYTES, 2) { return Err(Error::InvalidPrivateArtifactKind); } Ok(Self(value)) @@ -54,7 +65,7 @@ pub struct ArtifactSchemaId(String); impl ArtifactSchemaId { pub fn parse(value: impl Into<String>) -> Result<Self, Error> { let value = value.into(); - if !valid_label(value.as_str(), ARTIFACT_SCHEMA_MAX_BYTES) { + if !valid_schema(value.as_str()) { return Err(Error::InvalidPrivateArtifactSchema); } Ok(Self(value)) @@ -64,6 +75,79 @@ impl ArtifactSchemaId { } } +/// Opaque envelope context derived only from immutable artifact metadata. +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct PrivateArtifactEnvelopeContext { + purpose: String, + subject_type: &'static str, + subject: String, + payload_schema: String, +} + +impl PrivateArtifactEnvelopeContext { + fn derive( + artifact_id: PrivateArtifactId, + kind: &ArtifactKind, + schema_id: &ArtifactSchemaId, + ) -> Self { + Self { + purpose: format!( + "{PRIVATE_ARTIFACT_ENVELOPE_PURPOSE_PREFIX}{}", + kind.as_str() + ), + subject_type: PRIVATE_ARTIFACT_ENVELOPE_SUBJECT_TYPE, + subject: hex_artifact_id(artifact_id), + payload_schema: schema_id.as_str().to_owned(), + } + } + + pub fn purpose(&self) -> &str { + self.purpose.as_str() + } + pub const fn subject_type(&self) -> &'static str { + self.subject_type + } + pub fn subject(&self) -> &str { + self.subject.as_str() + } + pub fn payload_schema(&self) -> &str { + self.payload_schema.as_str() + } + pub fn fingerprint(&self) -> [u8; 32] { + Sha256::digest(self.canonical_bytes()).into() + } + + fn canonical_bytes(&self) -> Vec<u8> { + let mut encoded = Vec::new(); + encoded.extend_from_slice(&ENVELOPE_CONTEXT_VERSION.to_be_bytes()); + encoded.extend_from_slice(ENVELOPE_CONTEXT_DOMAIN); + for value in [ + self.purpose.as_bytes(), + self.subject_type.as_bytes(), + self.subject.as_bytes(), + self.payload_schema.as_bytes(), + ] { + let length = u16::try_from(value.len()) + .expect("validated private-artifact envelope context fits u16"); + encoded.extend_from_slice(&length.to_be_bytes()); + encoded.extend_from_slice(value); + } + encoded + } +} + +impl fmt::Debug for PrivateArtifactEnvelopeContext { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("PrivateArtifactEnvelopeContext") + .field("purpose", &"<derived>") + .field("subject_type", &self.subject_type) + .field("subject", &"<redacted>") + .field("payload_schema", &"<derived>") + .finish() + } +} + /// SHA-256 commitment to the exact protected representation. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] @@ -230,6 +314,224 @@ impl PrivateArtifactRevision { } } +/// Host-generated idempotency identity for one reseal commit. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct PrivateArtifactResealId([u8; 16]); + +impl PrivateArtifactResealId { + pub const fn new(bytes: [u8; 16]) -> Result<Self, Error> { + if bytes_are_zero(&bytes) { + return Err(Error::InvalidPrivateArtifactResealId); + } + Ok(Self(bytes)) + } + pub const fn as_bytes(&self) -> &[u8; 16] { + &self.0 + } +} + +impl fmt::Debug for PrivateArtifactResealId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("PrivateArtifactResealId(<redacted>)") + } +} + +/// Backend-neutral metadata fence for one atomic envelope reseal. +#[derive(Clone, Eq, PartialEq)] +pub struct PrivateArtifactResealRequest { + reseal_id: PrivateArtifactResealId, + artifact_id: PrivateArtifactId, + expected_revision: PrivateArtifactRevision, + expected_commitment: ArtifactCommitment, + next_commitment: ArtifactCommitment, + next_protected_size_bytes: u64, + next_secret_reference: DurableSecretReference, + committed_at_unix_ms: u64, +} + +impl fmt::Debug for PrivateArtifactResealRequest { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("PrivateArtifactResealRequest") + .field("reseal_id", &self.reseal_id) + .field("artifact_id", &self.artifact_id) + .field("expected_revision", &self.expected_revision) + .field("expected_commitment", &"<commitment>") + .field("next_commitment", &"<commitment>") + .field("next_protected_size_bytes", &self.next_protected_size_bytes) + .field("next_secret_reference", &self.next_secret_reference) + .field("committed_at_unix_ms", &self.committed_at_unix_ms) + .finish() + } +} + +impl PrivateArtifactResealRequest { + #[allow(clippy::too_many_arguments)] + pub fn new( + reseal_id: PrivateArtifactResealId, + artifact_id: PrivateArtifactId, + expected_revision: PrivateArtifactRevision, + expected_commitment: ArtifactCommitment, + next_commitment: ArtifactCommitment, + next_protected_size_bytes: u64, + next_secret_reference: DurableSecretReference, + committed_at_unix_ms: u64, + ) -> Result<Self, Error> { + if expected_commitment == next_commitment + || next_protected_size_bytes == 0 + || committed_at_unix_ms == 0 + { + return Err(Error::InvalidPrivateArtifactResealRequest); + } + Ok(Self { + reseal_id, + artifact_id, + expected_revision, + expected_commitment, + next_commitment, + next_protected_size_bytes, + next_secret_reference, + committed_at_unix_ms, + }) + } + + pub const fn reseal_id(&self) -> PrivateArtifactResealId { + self.reseal_id + } + pub const fn artifact_id(&self) -> PrivateArtifactId { + self.artifact_id + } + pub const fn expected_revision(&self) -> PrivateArtifactRevision { + self.expected_revision + } + pub const fn expected_commitment(&self) -> ArtifactCommitment { + self.expected_commitment + } + pub const fn next_commitment(&self) -> ArtifactCommitment { + self.next_commitment + } + pub const fn next_protected_size_bytes(&self) -> u64 { + self.next_protected_size_bytes + } + pub const fn next_secret_reference(&self) -> &DurableSecretReference { + &self.next_secret_reference + } + pub const fn committed_at_unix_ms(&self) -> u64 { + self.committed_at_unix_ms + } + pub fn fingerprint(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(self.reseal_id.as_bytes()); + hasher.update(self.artifact_id.as_bytes()); + hasher.update(self.expected_revision.get().to_be_bytes()); + hasher.update(self.expected_commitment.as_bytes()); + hasher.update(self.next_commitment.as_bytes()); + hasher.update(self.next_protected_size_bytes.to_be_bytes()); + hash_string(&mut hasher, self.next_secret_reference.provider()); + hash_string(&mut hasher, self.next_secret_reference.opaque_reference()); + hasher.update(self.next_secret_reference.key_version().to_be_bytes()); + hasher.update(self.committed_at_unix_ms.to_be_bytes()); + hasher.finalize().into() + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PrivateArtifactResealDisposition { + Committed, + Replayed, +} + +/// Durable receipt used to distinguish exact replay from conflicting reuse. +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct PrivateArtifactResealReceipt { + reseal_id: PrivateArtifactResealId, + artifact_id: PrivateArtifactId, + committed_revision: PrivateArtifactRevision, + request_fingerprint: [u8; 32], + disposition: PrivateArtifactResealDisposition, +} + +impl PrivateArtifactResealReceipt { + pub fn committed( + request: &PrivateArtifactResealRequest, + committed_revision: PrivateArtifactRevision, + ) -> Self { + Self { + reseal_id: request.reseal_id, + artifact_id: request.artifact_id, + committed_revision, + request_fingerprint: request.fingerprint(), + disposition: PrivateArtifactResealDisposition::Committed, + } + } + + pub fn replay(&self, request: &PrivateArtifactResealRequest) -> Result<Self, Error> { + if self.reseal_id != request.reseal_id + || self.artifact_id != request.artifact_id + || self.request_fingerprint != request.fingerprint() + { + return Err(Error::PrivateArtifactResealConflict); + } + Ok(Self { + disposition: PrivateArtifactResealDisposition::Replayed, + ..*self + }) + } + + pub const fn reseal_id(self) -> PrivateArtifactResealId { + self.reseal_id + } + pub const fn artifact_id(self) -> PrivateArtifactId { + self.artifact_id + } + pub const fn committed_revision(self) -> PrivateArtifactRevision { + self.committed_revision + } + pub const fn disposition(self) -> PrivateArtifactResealDisposition { + self.disposition + } + + pub const fn request_fingerprint(self) -> [u8; 32] { + self.request_fingerprint + } +} + +impl fmt::Debug for PrivateArtifactResealReceipt { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("PrivateArtifactResealReceipt") + .field("reseal_id", &self.reseal_id) + .field("artifact_id", &self.artifact_id) + .field("committed_revision", &self.committed_revision) + .field("request_fingerprint", &"<commitment>") + .field("disposition", &self.disposition) + .finish() + } +} + +/// Bounded migration inventory without artifact or user identity. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct PrivateArtifactEnvelopeMigrationStatus { + pub v1_pending: u64, + pub v2_current: u64, + pub corrupt: u64, + pub blocked_provider: u64, + pub conflicted: u64, +} + +impl PrivateArtifactEnvelopeMigrationStatus { + pub fn total(self) -> Option<u64> { + self.v1_pending + .checked_add(self.v2_current)? + .checked_add(self.corrupt)? + .checked_add(self.blocked_provider)? + .checked_add(self.conflicted) + } +} + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -361,10 +663,16 @@ impl PrivateArtifactMetadata { }, ); let valid = match (stage, revision.get(), tombstone) { - (PrivateArtifactStage::Active, 1, None) => updated_at_unix_ms == created_at_unix_ms, - (PrivateArtifactStage::Expired, 2, None) => retention.is_expired_at(updated_at_unix_ms), - (PrivateArtifactStage::Tombstoned, 2 | 3, Some(tombstone)) => { - tombstone.deleted_at_unix_ms == updated_at_unix_ms + (PrivateArtifactStage::Active, revision, None) => { + (revision == 1 && updated_at_unix_ms == created_at_unix_ms) + || (revision > 1 && updated_at_unix_ms > created_at_unix_ms) + } + (PrivateArtifactStage::Expired, revision, None) => { + revision >= 2 && retention.is_expired_at(updated_at_unix_ms) + } + (PrivateArtifactStage::Tombstoned, revision, Some(tombstone)) => { + revision >= 2 + && tombstone.deleted_at_unix_ms == updated_at_unix_ms && tombstone.commitment == commitment && retention.permits_deletion_at(updated_at_unix_ms) && (tombstone.reason != DeletionReason::RetentionExpired @@ -420,6 +728,32 @@ impl PrivateArtifactMetadata { self.tombstone } + /// Derives the only valid envelope context for this artifact. + pub fn envelope_context(&self) -> PrivateArtifactEnvelopeContext { + PrivateArtifactEnvelopeContext::derive(self.artifact_id, &self.kind, &self.schema_id) + } + + /// Applies the metadata half of a fenced envelope reseal. + pub fn resealed(&self, request: &PrivateArtifactResealRequest) -> Result<Self, Error> { + if self.stage != PrivateArtifactStage::Active + || request.artifact_id != self.artifact_id + || request.expected_revision != self.revision + || request.expected_commitment != self.commitment + { + return Err(Error::PrivateArtifactResealConflict); + } + if request.committed_at_unix_ms <= self.updated_at_unix_ms { + return Err(Error::InvalidPrivateArtifactTimestamp); + } + let mut next = self.clone(); + next.commitment = request.next_commitment; + next.protected_size_bytes = request.next_protected_size_bytes; + next.secret_reference = request.next_secret_reference.clone(); + next.revision = self.revision.next()?; + next.updated_at_unix_ms = request.committed_at_unix_ms; + Ok(next) + } + pub fn mark_expired( &self, expected_revision: PrivateArtifactRevision, @@ -505,6 +839,10 @@ pub trait PrivateArtifactStore: Send + Sync { &self, artifact_id: PrivateArtifactId, ) -> BoxFuture<'_, Result<Option<PrivateArtifactMetadata>, Error>>; + fn reseal_metadata( + &self, + request: PrivateArtifactResealRequest, + ) -> BoxFuture<'_, Result<PrivateArtifactResealReceipt, Error>>; fn mark_expired( &self, artifact_id: PrivateArtifactId, @@ -535,6 +873,47 @@ fn valid_label(value: &str, max: usize) -> bool { }) } +fn valid_schema(value: &str) -> bool { + if !valid_namespaced(value, ARTIFACT_SCHEMA_MAX_BYTES, 3) { + return false; + } + value.rsplit('.').next().is_some_and(|last| { + last.strip_prefix('v').is_some_and(|version| { + !version.is_empty() && version.bytes().all(|byte| byte.is_ascii_digit()) + }) + }) +} + +fn valid_namespaced(value: &str, max: usize, minimum_segments: usize) -> bool { + valid_label(value, max) + && value.split('.').count() >= minimum_segments + && value.split('.').all(|segment| { + let mut bytes = segment.bytes(); + bytes.next().is_some_and(|byte| byte.is_ascii_lowercase()) + && bytes.all(|byte| { + byte.is_ascii_lowercase() + || byte.is_ascii_digit() + || matches!(byte, b'_' | b'-') + }) + }) +} + +fn hex_artifact_id(artifact_id: PrivateArtifactId) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut encoded = String::with_capacity(32); + for byte in artifact_id.as_bytes() { + encoded.push(char::from(HEX[usize::from(byte >> 4)])); + encoded.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + encoded +} + +fn hash_string(hasher: &mut Sha256, value: &str) { + let length = u32::try_from(value.len()).expect("validated private-artifact field fits u32"); + hasher.update(length.to_be_bytes()); + hasher.update(value.as_bytes()); +} + const fn bytes_are_zero(bytes: &[u8; 16]) -> bool { let mut index = 0; while index < bytes.len() { diff --git a/crates/storage/tests/private_artifact.rs b/crates/storage/tests/private_artifact.rs @@ -2,11 +2,17 @@ use radroots_storage::{ Error, private_artifact::{ ArtifactCommitment, ArtifactKind, ArtifactSchemaId, DeletionReason, DurableSecretReference, - PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactRevision, PrivateArtifactStage, - PrivateArtifactStore, RetentionPolicy, + PrivateArtifactEnvelopeMigrationStatus, PrivateArtifactId, PrivateArtifactMetadata, + PrivateArtifactResealDisposition, PrivateArtifactResealId, PrivateArtifactResealRequest, + PrivateArtifactRevision, PrivateArtifactStage, PrivateArtifactStore, RetentionPolicy, }, }; +#[cfg(feature = "memory")] +use futures_executor::block_on; +#[cfg(feature = "memory")] +use radroots_storage::memory::MemoryStorage; + fn metadata(retention: RetentionPolicy) -> PrivateArtifactMetadata { PrivateArtifactMetadata::new( PrivateArtifactId::new([1; 16]).expect("artifact id"), @@ -417,3 +423,145 @@ fn transition_and_status_edge_matrix_is_complete() { None ); } + +#[test] +fn envelope_context_is_derived_and_transplant_resistant() { + let original = metadata(RetentionPolicy::indefinite()); + let context = original.envelope_context(); + assert_eq!( + context.purpose(), + "radroots.private_artifact.trade.private_terms" + ); + assert_eq!(context.subject_type(), "private_artifact"); + assert_eq!(context.subject(), "01010101010101010101010101010101"); + assert_eq!(context.payload_schema(), "trade.private_terms.v1"); + + let with_id = |id, kind, schema| { + PrivateArtifactMetadata::new( + PrivateArtifactId::new(id).unwrap(), + ArtifactKind::parse(kind).unwrap(), + ArtifactSchemaId::parse(schema).unwrap(), + ArtifactCommitment::new([2; 32]), + 512, + DurableSecretReference::new("keyring", "opaque-key-token", 3).unwrap(), + RetentionPolicy::indefinite(), + 100, + ) + .unwrap() + .envelope_context() + .fingerprint() + }; + let fingerprint = context.fingerprint(); + assert_ne!( + fingerprint, + with_id([3; 16], "trade.private_terms", "trade.private_terms.v1") + ); + assert_ne!( + fingerprint, + with_id([1; 16], "trade.other_terms", "trade.private_terms.v1") + ); + assert_ne!( + fingerprint, + with_id([1; 16], "trade.private_terms", "trade.private_terms.v2") + ); + + for invalid in ["trade..terms", ".trade.terms", "trade.1terms", "trade"] { + assert_eq!( + ArtifactKind::parse(invalid), + Err(Error::InvalidPrivateArtifactKind) + ); + } + for invalid in [ + "trade..terms.v1", + "trade.terms", + "trade.terms.latest", + "trade.terms.v", + ] { + assert_eq!( + ArtifactSchemaId::parse(invalid), + Err(Error::InvalidPrivateArtifactSchema) + ); + } + let diagnostic = format!("{context:?} {original:?}"); + assert!(!diagnostic.contains("01010101010101010101010101010101")); +} + +#[test] +#[cfg(feature = "memory")] +fn reseal_contract_distinguishes_exact_replay_and_conflict() { + let store = MemoryStorage::default(); + let initial = metadata(RetentionPolicy::indefinite()); + block_on(store.put_metadata(initial.clone())).unwrap(); + let request = PrivateArtifactResealRequest::new( + PrivateArtifactResealId::new([9; 16]).unwrap(), + initial.artifact_id(), + initial.revision(), + initial.commitment(), + ArtifactCommitment::new([8; 32]), + 640, + DurableSecretReference::new("keyring", "fresh-token", 4).unwrap(), + 200, + ) + .unwrap(); + let committed = block_on(store.reseal_metadata(request.clone())).unwrap(); + assert_eq!( + committed.disposition(), + PrivateArtifactResealDisposition::Committed + ); + assert_eq!(committed.committed_revision().get(), 2); + assert_eq!(committed.request_fingerprint(), request.fingerprint()); + + let replayed = block_on(store.reseal_metadata(request.clone())).unwrap(); + assert_eq!( + replayed.disposition(), + PrivateArtifactResealDisposition::Replayed + ); + assert_eq!( + replayed.committed_revision(), + committed.committed_revision() + ); + + let conflicting = PrivateArtifactResealRequest::new( + request.reseal_id(), + request.artifact_id(), + request.expected_revision(), + request.expected_commitment(), + ArtifactCommitment::new([7; 32]), + request.next_protected_size_bytes(), + request.next_secret_reference().clone(), + request.committed_at_unix_ms(), + ) + .unwrap(); + assert_eq!( + block_on(store.reseal_metadata(conflicting)), + Err(Error::PrivateArtifactResealConflict) + ); + assert_eq!( + PrivateArtifactResealId::new([0; 16]), + Err(Error::InvalidPrivateArtifactResealId) + ); +} + +#[test] +fn migration_status_is_bounded_and_overflow_safe() { + assert_eq!( + PrivateArtifactEnvelopeMigrationStatus { + v1_pending: 1, + v2_current: 2, + corrupt: 3, + blocked_provider: 4, + conflicted: 5, + } + .total(), + Some(15) + ); + assert_eq!( + PrivateArtifactEnvelopeMigrationStatus { + v1_pending: u64::MAX, + v2_current: 1, + ..PrivateArtifactEnvelopeMigrationStatus::default() + } + .total(), + None + ); +} diff --git a/docs/api/radroots_storage.txt b/docs/api/radroots_storage.txt @@ -769,6 +769,7 @@ pub fn radroots_storage::memory::MemoryStorage::expired(&self, u64, u16) -> radr pub fn radroots_storage::memory::MemoryStorage::mark_expired(&self, radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::PrivateArtifactRevision, u64) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::Error>> pub fn radroots_storage::memory::MemoryStorage::metadata(&self, radroots_storage::private_artifact::PrivateArtifactId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::private_artifact::PrivateArtifactMetadata>, radroots_storage::Error>> pub fn radroots_storage::memory::MemoryStorage::put_metadata(&self, radroots_storage::private_artifact::PrivateArtifactMetadata) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::Error>> +pub fn radroots_storage::memory::MemoryStorage::reseal_metadata(&self, radroots_storage::private_artifact::PrivateArtifactResealRequest) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactResealReceipt, radroots_storage::Error>> pub fn radroots_storage::memory::MemoryStorage::status(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactStatus, radroots_storage::Error>> pub fn radroots_storage::memory::MemoryStorage::tombstone(&self, radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::PrivateArtifactRevision, u64, radroots_storage::private_artifact::DeletionReason) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::Error>> impl radroots_storage::projection::ProjectionStore for radroots_storage::memory::MemoryStorage @@ -957,6 +958,9 @@ pub radroots_storage::private_artifact::DeletionReason::KeyRevoked pub radroots_storage::private_artifact::DeletionReason::OperatorRequested pub radroots_storage::private_artifact::DeletionReason::RetentionExpired pub radroots_storage::private_artifact::DeletionReason::UserRequested +pub enum radroots_storage::private_artifact::PrivateArtifactResealDisposition +pub radroots_storage::private_artifact::PrivateArtifactResealDisposition::Committed +pub radroots_storage::private_artifact::PrivateArtifactResealDisposition::Replayed pub enum radroots_storage::private_artifact::PrivateArtifactStage pub radroots_storage::private_artifact::PrivateArtifactStage::Active pub radroots_storage::private_artifact::PrivateArtifactStage::Expired @@ -990,20 +994,41 @@ impl serde_core::ser::Serialize for radroots_storage::private_artifact::DurableS pub fn radroots_storage::private_artifact::DurableSecretReference::serialize<S>(&self, S) -> core::result::Result<<S as serde_core::ser::Serializer>::Ok, <S as serde_core::ser::Serializer>::Error> where S: serde_core::ser::Serializer impl<'de> serde_core::de::Deserialize<'de> for radroots_storage::private_artifact::DurableSecretReference pub fn radroots_storage::private_artifact::DurableSecretReference::deserialize<D>(D) -> core::result::Result<Self, <D as serde_core::de::Deserializer>::Error> where D: serde_core::de::Deserializer<'de> +pub struct radroots_storage::private_artifact::PrivateArtifactEnvelopeContext +impl radroots_storage::private_artifact::PrivateArtifactEnvelopeContext +pub fn radroots_storage::private_artifact::PrivateArtifactEnvelopeContext::fingerprint(&self) -> [u8; 32] +pub fn radroots_storage::private_artifact::PrivateArtifactEnvelopeContext::payload_schema(&self) -> &str +pub fn radroots_storage::private_artifact::PrivateArtifactEnvelopeContext::purpose(&self) -> &str +pub fn radroots_storage::private_artifact::PrivateArtifactEnvelopeContext::subject(&self) -> &str +pub const fn radroots_storage::private_artifact::PrivateArtifactEnvelopeContext::subject_type(&self) -> &'static str +impl core::fmt::Debug for radroots_storage::private_artifact::PrivateArtifactEnvelopeContext +pub fn radroots_storage::private_artifact::PrivateArtifactEnvelopeContext::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_storage::private_artifact::PrivateArtifactEnvelopeMigrationStatus +pub radroots_storage::private_artifact::PrivateArtifactEnvelopeMigrationStatus::blocked_provider: u64 +pub radroots_storage::private_artifact::PrivateArtifactEnvelopeMigrationStatus::conflicted: u64 +pub radroots_storage::private_artifact::PrivateArtifactEnvelopeMigrationStatus::corrupt: u64 +pub radroots_storage::private_artifact::PrivateArtifactEnvelopeMigrationStatus::v1_pending: u64 +pub radroots_storage::private_artifact::PrivateArtifactEnvelopeMigrationStatus::v2_current: u64 +impl radroots_storage::private_artifact::PrivateArtifactEnvelopeMigrationStatus +pub fn radroots_storage::private_artifact::PrivateArtifactEnvelopeMigrationStatus::total(self) -> core::option::Option<u64> pub struct radroots_storage::private_artifact::PrivateArtifactId(_) impl radroots_storage::private_artifact::PrivateArtifactId pub const fn radroots_storage::private_artifact::PrivateArtifactId::as_bytes(&self) -> &[u8; 16] pub const fn radroots_storage::private_artifact::PrivateArtifactId::new([u8; 16]) -> core::result::Result<Self, radroots_storage::Error> +impl core::fmt::Debug for radroots_storage::private_artifact::PrivateArtifactId +pub fn radroots_storage::private_artifact::PrivateArtifactId::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct radroots_storage::private_artifact::PrivateArtifactMetadata impl radroots_storage::private_artifact::PrivateArtifactMetadata pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::artifact_id(&self) -> radroots_storage::private_artifact::PrivateArtifactId pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::commitment(&self) -> radroots_storage::private_artifact::ArtifactCommitment pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::created_at_unix_ms(&self) -> u64 +pub fn radroots_storage::private_artifact::PrivateArtifactMetadata::envelope_context(&self) -> radroots_storage::private_artifact::PrivateArtifactEnvelopeContext pub fn radroots_storage::private_artifact::PrivateArtifactMetadata::from_durable_parts(radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::ArtifactKind, radroots_storage::private_artifact::ArtifactSchemaId, radroots_storage::private_artifact::ArtifactCommitment, u64, radroots_storage::private_artifact::DurableSecretReference, radroots_storage::private_artifact::RetentionPolicy, radroots_storage::private_artifact::PrivateArtifactRevision, radroots_storage::private_artifact::PrivateArtifactStage, u64, u64, core::option::Option<(u64, radroots_storage::private_artifact::DeletionReason, radroots_storage::private_artifact::ArtifactCommitment)>) -> core::result::Result<Self, radroots_storage::Error> pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::kind(&self) -> &radroots_storage::private_artifact::ArtifactKind pub fn radroots_storage::private_artifact::PrivateArtifactMetadata::mark_expired(&self, radroots_storage::private_artifact::PrivateArtifactRevision, u64) -> core::result::Result<Self, radroots_storage::Error> pub fn radroots_storage::private_artifact::PrivateArtifactMetadata::new(radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::ArtifactKind, radroots_storage::private_artifact::ArtifactSchemaId, radroots_storage::private_artifact::ArtifactCommitment, u64, radroots_storage::private_artifact::DurableSecretReference, radroots_storage::private_artifact::RetentionPolicy, u64) -> core::result::Result<Self, radroots_storage::Error> pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::protected_size_bytes(&self) -> u64 +pub fn radroots_storage::private_artifact::PrivateArtifactMetadata::resealed(&self, &radroots_storage::private_artifact::PrivateArtifactResealRequest) -> core::result::Result<Self, radroots_storage::Error> pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::retention(&self) -> radroots_storage::private_artifact::RetentionPolicy pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::revision(&self) -> radroots_storage::private_artifact::PrivateArtifactRevision pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::schema_id(&self) -> &radroots_storage::private_artifact::ArtifactSchemaId @@ -1012,6 +1037,37 @@ pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::stage( pub fn radroots_storage::private_artifact::PrivateArtifactMetadata::tombstone(&self, radroots_storage::private_artifact::PrivateArtifactRevision, u64, radroots_storage::private_artifact::DeletionReason) -> core::result::Result<Self, radroots_storage::Error> pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::tombstone_record(&self) -> core::option::Option<radroots_storage::private_artifact::ArtifactTombstone> pub const fn radroots_storage::private_artifact::PrivateArtifactMetadata::updated_at_unix_ms(&self) -> u64 +pub struct radroots_storage::private_artifact::PrivateArtifactResealId(_) +impl radroots_storage::private_artifact::PrivateArtifactResealId +pub const fn radroots_storage::private_artifact::PrivateArtifactResealId::as_bytes(&self) -> &[u8; 16] +pub const fn radroots_storage::private_artifact::PrivateArtifactResealId::new([u8; 16]) -> core::result::Result<Self, radroots_storage::Error> +impl core::fmt::Debug for radroots_storage::private_artifact::PrivateArtifactResealId +pub fn radroots_storage::private_artifact::PrivateArtifactResealId::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_storage::private_artifact::PrivateArtifactResealReceipt +impl radroots_storage::private_artifact::PrivateArtifactResealReceipt +pub const fn radroots_storage::private_artifact::PrivateArtifactResealReceipt::artifact_id(self) -> radroots_storage::private_artifact::PrivateArtifactId +pub fn radroots_storage::private_artifact::PrivateArtifactResealReceipt::committed(&radroots_storage::private_artifact::PrivateArtifactResealRequest, radroots_storage::private_artifact::PrivateArtifactRevision) -> Self +pub const fn radroots_storage::private_artifact::PrivateArtifactResealReceipt::committed_revision(self) -> radroots_storage::private_artifact::PrivateArtifactRevision +pub const fn radroots_storage::private_artifact::PrivateArtifactResealReceipt::disposition(self) -> radroots_storage::private_artifact::PrivateArtifactResealDisposition +pub fn radroots_storage::private_artifact::PrivateArtifactResealReceipt::replay(&self, &radroots_storage::private_artifact::PrivateArtifactResealRequest) -> core::result::Result<Self, radroots_storage::Error> +pub const fn radroots_storage::private_artifact::PrivateArtifactResealReceipt::request_fingerprint(self) -> [u8; 32] +pub const fn radroots_storage::private_artifact::PrivateArtifactResealReceipt::reseal_id(self) -> radroots_storage::private_artifact::PrivateArtifactResealId +impl core::fmt::Debug for radroots_storage::private_artifact::PrivateArtifactResealReceipt +pub fn radroots_storage::private_artifact::PrivateArtifactResealReceipt::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_storage::private_artifact::PrivateArtifactResealRequest +impl radroots_storage::private_artifact::PrivateArtifactResealRequest +pub const fn radroots_storage::private_artifact::PrivateArtifactResealRequest::artifact_id(&self) -> radroots_storage::private_artifact::PrivateArtifactId +pub const fn radroots_storage::private_artifact::PrivateArtifactResealRequest::committed_at_unix_ms(&self) -> u64 +pub const fn radroots_storage::private_artifact::PrivateArtifactResealRequest::expected_commitment(&self) -> radroots_storage::private_artifact::ArtifactCommitment +pub const fn radroots_storage::private_artifact::PrivateArtifactResealRequest::expected_revision(&self) -> radroots_storage::private_artifact::PrivateArtifactRevision +pub fn radroots_storage::private_artifact::PrivateArtifactResealRequest::fingerprint(&self) -> [u8; 32] +pub fn radroots_storage::private_artifact::PrivateArtifactResealRequest::new(radroots_storage::private_artifact::PrivateArtifactResealId, radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::PrivateArtifactRevision, radroots_storage::private_artifact::ArtifactCommitment, radroots_storage::private_artifact::ArtifactCommitment, u64, radroots_storage::private_artifact::DurableSecretReference, u64) -> core::result::Result<Self, radroots_storage::Error> +pub const fn radroots_storage::private_artifact::PrivateArtifactResealRequest::next_commitment(&self) -> radroots_storage::private_artifact::ArtifactCommitment +pub const fn radroots_storage::private_artifact::PrivateArtifactResealRequest::next_protected_size_bytes(&self) -> u64 +pub const fn radroots_storage::private_artifact::PrivateArtifactResealRequest::next_secret_reference(&self) -> &radroots_storage::private_artifact::DurableSecretReference +pub const fn radroots_storage::private_artifact::PrivateArtifactResealRequest::reseal_id(&self) -> radroots_storage::private_artifact::PrivateArtifactResealId +impl core::fmt::Debug for radroots_storage::private_artifact::PrivateArtifactResealRequest +pub fn radroots_storage::private_artifact::PrivateArtifactResealRequest::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct radroots_storage::private_artifact::PrivateArtifactRevision(_) impl radroots_storage::private_artifact::PrivateArtifactRevision pub const radroots_storage::private_artifact::PrivateArtifactRevision::INITIAL: Self @@ -1034,6 +1090,8 @@ pub const fn radroots_storage::private_artifact::RetentionPolicy::permits_deleti pub const radroots_storage::private_artifact::ARTIFACT_KIND_MAX_BYTES: usize pub const radroots_storage::private_artifact::ARTIFACT_SCHEMA_MAX_BYTES: usize pub const radroots_storage::private_artifact::EXPIRED_ARTIFACT_QUERY_LIMIT_MAX: u16 +pub const radroots_storage::private_artifact::PRIVATE_ARTIFACT_ENVELOPE_PURPOSE_PREFIX: &str +pub const radroots_storage::private_artifact::PRIVATE_ARTIFACT_ENVELOPE_SUBJECT_TYPE: &str pub const radroots_storage::private_artifact::SECRET_PROVIDER_MAX_BYTES: usize pub const radroots_storage::private_artifact::SECRET_REFERENCE_MAX_BYTES: usize pub trait radroots_storage::private_artifact::PrivateArtifactStore: core::marker::Send + core::marker::Sync @@ -1041,6 +1099,7 @@ pub fn radroots_storage::private_artifact::PrivateArtifactStore::expired(&self, pub fn radroots_storage::private_artifact::PrivateArtifactStore::mark_expired(&self, radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::PrivateArtifactRevision, u64) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::Error>> pub fn radroots_storage::private_artifact::PrivateArtifactStore::metadata(&self, radroots_storage::private_artifact::PrivateArtifactId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::private_artifact::PrivateArtifactMetadata>, radroots_storage::Error>> pub fn radroots_storage::private_artifact::PrivateArtifactStore::put_metadata(&self, radroots_storage::private_artifact::PrivateArtifactMetadata) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::Error>> +pub fn radroots_storage::private_artifact::PrivateArtifactStore::reseal_metadata(&self, radroots_storage::private_artifact::PrivateArtifactResealRequest) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactResealReceipt, radroots_storage::Error>> pub fn radroots_storage::private_artifact::PrivateArtifactStore::status(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactStatus, radroots_storage::Error>> pub fn radroots_storage::private_artifact::PrivateArtifactStore::tombstone(&self, radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::PrivateArtifactRevision, u64, radroots_storage::private_artifact::DeletionReason) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::Error>> impl radroots_storage::private_artifact::PrivateArtifactStore for radroots_storage::memory::MemoryStorage @@ -1048,6 +1107,7 @@ pub fn radroots_storage::memory::MemoryStorage::expired(&self, u64, u16) -> radr pub fn radroots_storage::memory::MemoryStorage::mark_expired(&self, radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::PrivateArtifactRevision, u64) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::Error>> pub fn radroots_storage::memory::MemoryStorage::metadata(&self, radroots_storage::private_artifact::PrivateArtifactId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::private_artifact::PrivateArtifactMetadata>, radroots_storage::Error>> pub fn radroots_storage::memory::MemoryStorage::put_metadata(&self, radroots_storage::private_artifact::PrivateArtifactMetadata) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::Error>> +pub fn radroots_storage::memory::MemoryStorage::reseal_metadata(&self, radroots_storage::private_artifact::PrivateArtifactResealRequest) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactResealReceipt, radroots_storage::Error>> pub fn radroots_storage::memory::MemoryStorage::status(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactStatus, radroots_storage::Error>> pub fn radroots_storage::memory::MemoryStorage::tombstone(&self, radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::PrivateArtifactRevision, u64, radroots_storage::private_artifact::DeletionReason) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::Error>> pub mod radroots_storage::projection @@ -1343,6 +1403,8 @@ pub radroots_storage::Error::InvalidOutboxTimestamp pub radroots_storage::Error::InvalidPrivateArtifactId pub radroots_storage::Error::InvalidPrivateArtifactKind pub radroots_storage::Error::InvalidPrivateArtifactMetadata +pub radroots_storage::Error::InvalidPrivateArtifactResealId +pub radroots_storage::Error::InvalidPrivateArtifactResealRequest pub radroots_storage::Error::InvalidPrivateArtifactRetention pub radroots_storage::Error::InvalidPrivateArtifactRevision pub radroots_storage::Error::InvalidPrivateArtifactSchema @@ -1379,6 +1441,8 @@ pub radroots_storage::Error::OutboxRevisionConflict pub radroots_storage::Error::PrivateArtifactConflict pub radroots_storage::Error::PrivateArtifactNotExpired pub radroots_storage::Error::PrivateArtifactNotFound +pub radroots_storage::Error::PrivateArtifactPersistenceIndeterminate +pub radroots_storage::Error::PrivateArtifactResealConflict pub radroots_storage::Error::PrivateArtifactRetentionActive pub radroots_storage::Error::PrivateArtifactRevisionConflict pub radroots_storage::Error::PrivateArtifactTombstoned