lib

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

authored_draft_submission.rs (11678B)


      1 //! Atomic association of an immutable captured draft with an authored preparation.
      2 //!
      3 //! The application owns the complete semantic request in the intent payload.
      4 //! Storage compares that payload and every preparation field on replay. A command
      5 //! key is scoped by the stable author, never by a process generation or a digest.
      6 use crate::{
      7     Error,
      8     atomic::AtomicCommitId,
      9     authored::{ArtifactOrigin, SigningState},
     10     authored_atomic::PrepareAuthoredOperation,
     11     authored_draft::{AuthoredDraft, AuthoredDraftId, AuthoredDraftRevision, AuthoredDraftStage},
     12     authored_draft_query::{AuthoredDraftQuery, AuthoredDraftScope},
     13 };
     14 use sha2::{Digest, Sha256};
     15 
     16 /// Captured source metadata; contains no application payload or credentials.
     17 #[derive(Clone, Debug, Eq, PartialEq)]
     18 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     19 #[cfg_attr(feature = "serde", serde(try_from = "SourceWire", into = "SourceWire"))]
     20 pub struct AuthoredDraftSource {
     21     draft_id: AuthoredDraftId,
     22     revision: AuthoredDraftRevision,
     23     author: [u8; 32],
     24     payload_schema: String,
     25     scope: Option<AuthoredDraftScope>,
     26     payload_sha256: [u8; 32],
     27     stage: AuthoredDraftStage,
     28     created_at_unix_ms: u64,
     29     updated_at_unix_ms: u64,
     30 }
     31 impl AuthoredDraftSource {
     32     pub fn capture(draft: &AuthoredDraft) -> Result<Self, Error> {
     33         draft.validate()?;
     34         let value = Self {
     35             draft_id: draft.draft_id(),
     36             revision: draft.revision(),
     37             author: *draft.author(),
     38             payload_schema: draft.payload_schema().to_owned(),
     39             scope: draft.scope(),
     40             payload_sha256: *draft.payload_sha256(),
     41             stage: draft.stage(),
     42             created_at_unix_ms: draft.created_at_unix_ms(),
     43             updated_at_unix_ms: draft.updated_at_unix_ms(),
     44         };
     45         value.validate()?;
     46         Ok(value)
     47     }
     48     fn validate(&self) -> Result<(), Error> {
     49         AuthoredDraftQuery::new(self.author, &self.payload_schema, self.scope, 1)?;
     50         if !matches!(
     51             self.stage,
     52             AuthoredDraftStage::Draft
     53                 | AuthoredDraftStage::MediaPreparing
     54                 | AuthoredDraftStage::MediaUploading
     55         ) || self.created_at_unix_ms == 0
     56             || self.updated_at_unix_ms < self.created_at_unix_ms
     57         {
     58             return Err(Error::InvalidAuthoredDraft);
     59         }
     60         Ok(())
     61     }
     62     pub fn matches(&self, draft: &AuthoredDraft) -> bool {
     63         Self::capture(draft).is_ok_and(|captured| captured == *self)
     64     }
     65     pub const fn draft_id(&self) -> AuthoredDraftId {
     66         self.draft_id
     67     }
     68     pub const fn revision(&self) -> AuthoredDraftRevision {
     69         self.revision
     70     }
     71     pub const fn author(&self) -> &[u8; 32] {
     72         &self.author
     73     }
     74     pub fn payload_schema(&self) -> &str {
     75         &self.payload_schema
     76     }
     77     pub const fn scope(&self) -> Option<AuthoredDraftScope> {
     78         self.scope
     79     }
     80     pub const fn payload_sha256(&self) -> &[u8; 32] {
     81         &self.payload_sha256
     82     }
     83 }
     84 
     85 /// A captured request and its distinct immutable intent, installed without effects.
     86 #[derive(Clone, Eq, PartialEq)]
     87 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     88 #[cfg_attr(
     89     feature = "serde",
     90     serde(try_from = "SubmissionWire", into = "SubmissionWire")
     91 )]
     92 pub struct PrepareFromDraft {
     93     command_id: AtomicCommitId,
     94     source: AuthoredDraftSource,
     95     intent: AuthoredDraft,
     96     preparation: PrepareAuthoredOperation,
     97 }
     98 impl PrepareFromDraft {
     99     /// Captures an intent whose prerequisites are complete and operation is associated.
    100     pub fn new(
    101         command_id: AtomicCommitId,
    102         source: AuthoredDraftSource,
    103         intent: AuthoredDraft,
    104         preparation: PrepareAuthoredOperation,
    105     ) -> Result<Self, Error> {
    106         if !matches!(
    107             intent.stage(),
    108             AuthoredDraftStage::ReadyToSign | AuthoredDraftStage::Queued
    109         ) {
    110             return Err(Error::AtomicWorkflowMismatch);
    111         }
    112         Self::from_parts(command_id, source, intent, preparation)
    113     }
    114 
    115     /// Captures an operation before caller-owned prerequisites are complete.
    116     ///
    117     /// The waiting draft has no operation association or signing authority. Its
    118     /// original complete snapshot and prepared operation are bound immutably in
    119     /// the composite receipt. The caller owns prerequisite progression and must
    120     /// later associate that same operation without changing its captured plan.
    121     pub fn new_waiting(
    122         command_id: AtomicCommitId,
    123         source: AuthoredDraftSource,
    124         intent: AuthoredDraft,
    125         preparation: PrepareAuthoredOperation,
    126     ) -> Result<Self, Error> {
    127         if !matches!(
    128             intent.stage(),
    129             AuthoredDraftStage::Draft
    130                 | AuthoredDraftStage::MediaPreparing
    131                 | AuthoredDraftStage::MediaUploading
    132         ) {
    133             return Err(Error::AtomicWorkflowMismatch);
    134         }
    135         Self::from_parts(command_id, source, intent, preparation)
    136     }
    137 
    138     fn from_parts(
    139         command_id: AtomicCommitId,
    140         source: AuthoredDraftSource,
    141         intent: AuthoredDraft,
    142         preparation: PrepareAuthoredOperation,
    143     ) -> Result<Self, Error> {
    144         let value = Self {
    145             command_id,
    146             source,
    147             intent,
    148             preparation,
    149         };
    150         value.validate()?;
    151         Ok(value)
    152     }
    153     pub fn validate(&self) -> Result<(), Error> {
    154         AtomicCommitId::new(*self.command_id.as_bytes())?;
    155         self.source.validate()?;
    156         self.intent.validate()?;
    157         let operation = self.preparation.operation();
    158         let at = self.preparation.requested_at_unix_ms();
    159         let operation_matches = match self.intent.stage() {
    160             AuthoredDraftStage::Draft
    161             | AuthoredDraftStage::MediaPreparing
    162             | AuthoredDraftStage::MediaUploading => self.intent.operation_id().is_none(),
    163             AuthoredDraftStage::ReadyToSign | AuthoredDraftStage::Queued => {
    164                 self.intent.operation_id() == Some(operation.operation_id())
    165             }
    166             AuthoredDraftStage::Cancelled => false,
    167         };
    168         if self.intent.draft_id() == self.source.draft_id
    169             || self.intent.author() != &self.source.author
    170             || self.intent.scope() != self.source.scope
    171             || self.intent.revision() != AuthoredDraftRevision::INITIAL
    172             || !operation_matches
    173             || self.intent.created_at_unix_ms() != at
    174             || self.intent.updated_at_unix_ms() != at
    175             || at < self.source.updated_at_unix_ms
    176             || operation.created_at_unix_ms() != at
    177             || operation.updated_at_unix_ms() != at
    178             || operation.revision().get() != 1
    179         {
    180             return Err(Error::AtomicWorkflowMismatch);
    181         }
    182         for artifact in self.preparation.artifacts() {
    183             if artifact.origin() != ArtifactOrigin::Planned
    184                 || artifact.signing_state() != SigningState::Planned
    185                 || artifact.revision().get() != 1
    186                 || artifact.created_at_unix_ms() != at
    187                 || artifact.updated_at_unix_ms() != at
    188                 || artifact
    189                     .plan()
    190                     .ok_or(Error::AtomicWorkflowMismatch)?
    191                     .decode()?
    192                     .plan()
    193                     .author()
    194                     .as_bytes()
    195                     != &self.source.author
    196             {
    197                 return Err(Error::AtomicWorkflowMismatch);
    198             }
    199         }
    200         if self.preparation.delivery_plans().iter().any(|plan| {
    201             plan.revision().get() != 1
    202                 || plan.created_at_unix_ms() != at
    203                 || plan.updated_at_unix_ms() != at
    204         }) {
    205             return Err(Error::AtomicWorkflowMismatch);
    206         }
    207         Ok(())
    208     }
    209     /// Stable lookup key, available before persistence and after restart.
    210     pub fn commit_id_for(author: &[u8; 32], command_id: AtomicCommitId) -> AtomicCommitId {
    211         let mut hash = Sha256::new();
    212         hash.update(b"radroots.authored.draft.submission.id.v1\0");
    213         hash.update(author);
    214         hash.update(command_id.as_bytes());
    215         let digest = hash.finalize();
    216         let mut id = [0; 16];
    217         id.copy_from_slice(&digest[..16]);
    218         AtomicCommitId::new(id).expect("SHA-256 derived submission identity is nonzero")
    219     }
    220     pub fn commit_id(&self) -> AtomicCommitId {
    221         Self::commit_id_for(&self.source.author, self.command_id)
    222     }
    223     pub const fn command_id(&self) -> AtomicCommitId {
    224         self.command_id
    225     }
    226     pub const fn source(&self) -> &AuthoredDraftSource {
    227         &self.source
    228     }
    229     pub const fn intent(&self) -> &AuthoredDraft {
    230         &self.intent
    231     }
    232     pub const fn preparation(&self) -> &PrepareAuthoredOperation {
    233         &self.preparation
    234     }
    235 }
    236 impl core::fmt::Debug for PrepareFromDraft {
    237     fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
    238         formatter
    239             .debug_struct("PrepareFromDraft")
    240             .field("command_id", &self.command_id)
    241             .field("source", &self.source)
    242             .field("intent_id", &self.intent.draft_id())
    243             .field("operation_id", &self.preparation.operation().operation_id())
    244             .finish_non_exhaustive()
    245     }
    246 }
    247 
    248 #[cfg(feature = "serde")]
    249 #[derive(serde::Serialize, serde::Deserialize)]
    250 #[serde(deny_unknown_fields)]
    251 struct SourceWire {
    252     draft_id: AuthoredDraftId,
    253     revision: AuthoredDraftRevision,
    254     author: [u8; 32],
    255     payload_schema: String,
    256     scope: Option<AuthoredDraftScope>,
    257     payload_sha256: [u8; 32],
    258     stage: AuthoredDraftStage,
    259     created_at_unix_ms: u64,
    260     updated_at_unix_ms: u64,
    261 }
    262 #[cfg(feature = "serde")]
    263 impl TryFrom<SourceWire> for AuthoredDraftSource {
    264     type Error = Error;
    265     fn try_from(v: SourceWire) -> Result<Self, Error> {
    266         let value = Self {
    267             draft_id: v.draft_id,
    268             revision: v.revision,
    269             author: v.author,
    270             payload_schema: v.payload_schema,
    271             scope: v.scope,
    272             payload_sha256: v.payload_sha256,
    273             stage: v.stage,
    274             created_at_unix_ms: v.created_at_unix_ms,
    275             updated_at_unix_ms: v.updated_at_unix_ms,
    276         };
    277         value.validate()?;
    278         Ok(value)
    279     }
    280 }
    281 #[cfg(feature = "serde")]
    282 impl From<AuthoredDraftSource> for SourceWire {
    283     fn from(v: AuthoredDraftSource) -> Self {
    284         Self {
    285             draft_id: v.draft_id,
    286             revision: v.revision,
    287             author: v.author,
    288             payload_schema: v.payload_schema,
    289             scope: v.scope,
    290             payload_sha256: v.payload_sha256,
    291             stage: v.stage,
    292             created_at_unix_ms: v.created_at_unix_ms,
    293             updated_at_unix_ms: v.updated_at_unix_ms,
    294         }
    295     }
    296 }
    297 #[cfg(feature = "serde")]
    298 #[derive(serde::Serialize, serde::Deserialize)]
    299 #[serde(deny_unknown_fields)]
    300 struct SubmissionWire {
    301     command_id: AtomicCommitId,
    302     source: AuthoredDraftSource,
    303     intent: AuthoredDraft,
    304     preparation: PrepareAuthoredOperation,
    305 }
    306 #[cfg(feature = "serde")]
    307 impl TryFrom<SubmissionWire> for PrepareFromDraft {
    308     type Error = Error;
    309     fn try_from(v: SubmissionWire) -> Result<Self, Error> {
    310         Self::from_parts(v.command_id, v.source, v.intent, v.preparation)
    311     }
    312 }
    313 #[cfg(feature = "serde")]
    314 impl From<PrepareFromDraft> for SubmissionWire {
    315     fn from(v: PrepareFromDraft) -> Self {
    316         Self {
    317             command_id: v.command_id,
    318             source: v.source,
    319             intent: v.intent,
    320             preparation: v.preparation,
    321         }
    322     }
    323 }