Skip to main content

prikk_object/
envelope.rs

1//! Object envelope.
2
3use prikk_error::{PrikkError, Result};
4
5use crate::{CanonicalEncode, CanonicalWriter, ObjectId, ObjectType, Signature};
6
7/// Object envelope containing unsigned canonical payload bytes plus external signatures.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ObjectEnvelope {
10    /// Object type.
11    pub object_type: ObjectType,
12    /// Schema version.
13    pub schema_version: u32,
14    /// Unsigned canonical payload bytes.
15    pub canonical_payload: Vec<u8>,
16    /// Signatures over the object ID. Signatures are not part of the object ID preimage.
17    pub signatures: Vec<Signature>,
18}
19
20impl ObjectEnvelope {
21    /// Construct a new unsigned envelope.
22    #[must_use]
23    pub fn unsigned(
24        object_type: ObjectType,
25        schema_version: u32,
26        canonical_payload: Vec<u8>,
27    ) -> Self {
28        Self {
29            object_type,
30            schema_version,
31            canonical_payload,
32            signatures: Vec::new(),
33        }
34    }
35
36    /// Compute this envelope's object ID from its unsigned payload.
37    #[must_use]
38    pub fn object_id(&self) -> ObjectId {
39        ObjectId::from_canonical_payload(
40            self.object_type,
41            self.schema_version,
42            &self.canonical_payload,
43        )
44    }
45
46    /// Validate envelope metadata and signatures structurally.
47    pub fn validate(&self) -> Result<()> {
48        if self.schema_version == 0 {
49            return Err(PrikkError::UnsupportedFormatVersion(0));
50        }
51        for signature in &self.signatures {
52            signature.validate()?;
53        }
54        Ok(())
55    }
56
57    /// Append a signature. This does not recompute or change the object ID.
58    pub fn add_signature(&mut self, signature: Signature) -> Result<()> {
59        signature.validate()?;
60        self.signatures.push(signature);
61        self.signatures.sort_by(|a, b| {
62            (&a.key_id, a.signer_role, a.created_at).cmp(&(&b.key_id, b.signer_role, b.created_at))
63        });
64        Ok(())
65    }
66}
67
68impl CanonicalEncode for ObjectEnvelope {
69    fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
70        writer.field_u32(1, self.object_type.code() as u32)?;
71        writer.field_u32(2, self.schema_version)?;
72        writer.field_bytes(3, &self.canonical_payload)?;
73        writer.repeated_record(4, &self.signatures)?;
74        Ok(())
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::ObjectEnvelope;
81    use crate::{ObjectType, Signature, SignatureAlgorithm, SignerRole};
82
83    #[test]
84    fn signature_does_not_change_object_id() {
85        let mut envelope = ObjectEnvelope::unsigned(ObjectType::Patch, 1, b"payload".to_vec());
86        let before = envelope.object_id();
87        let signature = Signature {
88            algorithm: SignatureAlgorithm::Ed25519,
89            key_id: "k1".to_string(),
90            signature_bytes: vec![1, 2, 3],
91            created_at: 1,
92            signer_role: SignerRole::Author,
93        };
94        assert!(envelope.add_signature(signature).is_ok());
95        assert_eq!(before, envelope.object_id());
96    }
97}