heddle_object_model/object/thread_replication/
source_author.rs1use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use super::{Capture, metadata::AUTHORITY_FORMAT};
7use crate::{
8 error::{HeddleError, Result},
9 object::{CollaborationActor, ContentHash},
10};
11
12pub const MAX_SOURCE_AUTHORITY_BYTES: usize = 64 * 1024;
13pub const SOURCE_AUTHORIZATION_METHOD: &str = "/heddle.api.v1alpha2.SyncService/PublishContent";
15
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
18pub enum SourceAuthor {
19 LocalKey,
22 Account {
23 spool: Uuid,
24 actor: CollaborationActor,
25 authority_digest: ContentHash,
26 #[serde(with = "serde_bytes")]
27 authority: Vec<u8>,
28 },
29}
30impl SourceAuthor {
31 pub fn account(spool: Uuid, actor: CollaborationActor, authority: Vec<u8>) -> Result<Self> {
32 let value = Self::Account {
33 spool,
34 actor,
35 authority_digest: ContentHash::compute_typed(AUTHORITY_FORMAT, &authority),
36 authority,
37 };
38 value.validate()?;
39 Ok(value)
40 }
41
42 pub fn validate(&self) -> Result<()> {
43 if let Self::Account {
44 spool,
45 actor,
46 authority_digest,
47 authority,
48 } = self
49 {
50 if spool.is_nil()
51 || actor.principal_id.is_nil()
52 || actor.agent_id.as_ref().is_some_and(|id| {
53 id.is_empty() || id.len() > 256 || id.chars().any(char::is_control)
54 })
55 {
56 return Err(invalid("invalid original source author identity"));
57 }
58 if authority.is_empty()
59 || authority.len() > MAX_SOURCE_AUTHORITY_BYTES
60 || ContentHash::compute_typed(AUTHORITY_FORMAT, authority) != *authority_digest
61 {
62 return Err(invalid("invalid original source authority binding"));
63 }
64 }
65 Ok(())
66 }
67}
68
69#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct AuthoredCapture {
74 pub result: Capture,
75 pub author: SourceAuthor,
76}
77impl AuthoredCapture {
78 pub fn local(result: Capture) -> Self {
79 Self {
80 result,
81 author: SourceAuthor::LocalKey,
82 }
83 }
84 pub fn account(
85 result: Capture,
86 spool: Uuid,
87 actor: CollaborationActor,
88 authority: Vec<u8>,
89 ) -> Result<Self> {
90 Ok(Self {
91 result,
92 author: SourceAuthor::account(spool, actor, authority)?,
93 })
94 }
95}
96fn invalid(message: &str) -> HeddleError {
97 HeddleError::InvalidObject(message.into())
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 fn actor() -> CollaborationActor {
105 CollaborationActor {
106 principal_id: Uuid::from_u128(1),
107 agent_id: Some("capture-agent".into()),
108 }
109 }
110 #[test]
111 fn original_source_authority_binds_exact_envelope_and_bounded_identity() {
112 let author = SourceAuthor::account(Uuid::from_u128(2), actor(), vec![3; 32])
113 .expect("bounded authority");
114 let SourceAuthor::Account {
115 authority_digest, ..
116 } = &author
117 else {
118 panic!("account author")
119 };
120 assert_eq!(
121 *authority_digest,
122 ContentHash::compute_typed(AUTHORITY_FORMAT, &[3; 32])
123 );
124 let mut changed = author;
125 let SourceAuthor::Account { authority, .. } = &mut changed else {
126 panic!("account author")
127 };
128 authority[0] ^= 1;
129 assert!(
130 changed.validate().is_err(),
131 "authority bytes cannot change under original digest"
132 );
133 assert!(SourceAuthor::account(Uuid::nil(), actor(), vec![1]).is_err());
134 assert!(SourceAuthor::account(Uuid::from_u128(2), actor(), vec![]).is_err());
135 assert!(
136 SourceAuthor::account(
137 Uuid::from_u128(2),
138 actor(),
139 vec![1; MAX_SOURCE_AUTHORITY_BYTES + 1]
140 )
141 .is_err()
142 );
143 let mut invalid_agent = actor();
144 invalid_agent.agent_id = Some("\n".into());
145 assert!(SourceAuthor::account(Uuid::from_u128(2), invalid_agent, vec![1]).is_err());
146 SourceAuthor::LocalKey
147 .validate()
148 .expect("local key needs no account enrollment");
149 }
150}