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:
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