1#[path = "original_boundary_preflight.rs"]
4mod preflight;
5
6use std::collections::BTreeSet;
7
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use super::{
12 ContentHash, StateId,
13 thread_authority_admission::OriginalAuthorityBinding,
14 thread_replication::{
15 GenesisOwner, SourceAuthor, ThreadGenesis, ThreadOperation, metadata::AUTHORITY_FORMAT,
16 ownership_claim::ThreadOwnershipClaim, ownership_resolution::ThreadOwnershipResolution,
17 },
18};
19use crate::error::{HeddleError, Result};
20
21pub const FORMAT: &str = "heddle-original-boundary-acceptance-v1";
22pub const MANIFEST_FORMAT: &str = "heddle-original-publication-manifest-v1";
23pub const INTENT_FORMAT: &str = "heddle-original-publication-intent-v1";
24pub const MAX_RECORDS: usize = 10_384;
25pub const MAX_MANIFEST_BYTES: usize = 16 * 1024 * 1024;
26pub const MAX_ACCEPTANCE_BYTES: usize = 96 * 1024;
27
28#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
31pub enum ManifestSubject {
32 Genesis(ContentHash),
33 Source(ContentHash),
34 OtherOperation(ContentHash),
35 OwnershipClaim(ContentHash),
36 OwnershipResolution(ContentHash),
37}
38impl ManifestSubject {
39 pub fn id(&self) -> ContentHash {
40 match self {
41 Self::Genesis(id)
42 | Self::Source(id)
43 | Self::OtherOperation(id)
44 | Self::OwnershipClaim(id)
45 | Self::OwnershipResolution(id) => *id,
46 }
47 }
48}
49#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct OriginalManifestEntry {
52 pub subject: ManifestSubject,
53 pub thread: ContentHash,
54 pub publisher: [u8; 32],
55 pub authority: Option<OriginalAuthorityBinding>,
56}
57impl OriginalManifestEntry {
58 pub fn from_operation(operation: &ThreadOperation) -> Result<Self> {
60 let id = operation.id()?;
61 Ok(Self {
62 subject: if operation.source_result()?.is_some() {
63 ManifestSubject::Source(id)
64 } else {
65 ManifestSubject::OtherOperation(id)
66 },
67 thread: operation.thread,
68 publisher: operation.publisher,
69 authority: OriginalAuthorityBinding::from_operation(operation)?,
70 })
71 }
72 pub fn from_genesis(genesis: &ThreadGenesis, creator_authority: &[u8]) -> Result<Self> {
76 let authority = match genesis.owner {
77 GenesisOwner::Account(account) => {
78 if creator_authority.is_empty() || creator_authority.len() > 64 * 1024 {
79 return Err(invalid(
80 "account genesis requires bounded creator authority",
81 ));
82 }
83 Some(OriginalAuthorityBinding {
84 spool: Uuid::parse_str(&genesis.spool)
85 .map_err(|_| invalid("invalid genesis Spool"))?,
86 actor: super::CollaborationActor {
87 principal_id: account,
88 agent_id: None,
89 },
90 authority_digest: ContentHash::compute_typed(
91 AUTHORITY_FORMAT,
92 creator_authority,
93 ),
94 })
95 }
96 GenesisOwner::LocalKey(_) => {
97 if !creator_authority.is_empty() {
98 return Err(invalid("local genesis has no account authority"));
99 }
100 None
101 }
102 };
103 let id = genesis.id()?;
104 Ok(Self {
105 subject: ManifestSubject::Genesis(id),
106 thread: id,
107 publisher: genesis.creator,
108 authority,
109 })
110 }
111 pub fn from_claim(claim: &ThreadOwnershipClaim) -> Result<Self> {
113 claim.encode()?;
114 let SourceAuthor::Account {
115 spool,
116 actor,
117 authority_digest,
118 ..
119 } = &claim.acceptance
120 else {
121 return Err(invalid("claim requires explicit account authority"));
122 };
123 Ok(Self {
124 subject: ManifestSubject::OwnershipClaim(claim.id()?),
125 thread: claim.thread,
126 publisher: claim.accepting_publisher,
127 authority: Some(OriginalAuthorityBinding {
128 spool: *spool,
129 actor: actor.clone(),
130 authority_digest: *authority_digest,
131 }),
132 })
133 }
134 pub fn from_resolution(resolution: &ThreadOwnershipResolution) -> Result<Self> {
135 resolution.encode()?;
136 let SourceAuthor::Account {
137 spool,
138 actor,
139 authority_digest,
140 ..
141 } = &resolution.acceptance
142 else {
143 return Err(invalid("resolution requires explicit account acceptance"));
144 };
145 Ok(Self {
146 subject: ManifestSubject::OwnershipResolution(resolution.id()?),
147 thread: resolution.thread,
148 publisher: resolution.accepting_publisher,
149 authority: Some(OriginalAuthorityBinding {
150 spool: *spool,
151 actor: actor.clone(),
152 authority_digest: *authority_digest,
153 }),
154 })
155 }
156 fn validate(&self) -> Result<()> {
157 if self.publisher == [0; 32]
158 || self.subject.id().as_bytes() == &[0; 32]
159 || self.thread.as_bytes() == &[0; 32]
160 {
161 return Err(invalid("invalid original manifest identity"));
162 }
163 if matches!(self.subject, ManifestSubject::Genesis(_))
164 && (self.subject.id() != self.thread
165 || self
166 .authority
167 .as_ref()
168 .is_some_and(|binding| binding.actor.agent_id.is_some()))
169 {
170 return Err(invalid("genesis manifest cannot assert an unsigned agent"));
171 }
172 if let Some(binding) = &self.authority
173 && (binding.spool.is_nil()
174 || binding.actor.principal_id.is_nil()
175 || binding.actor.agent_id.as_ref().is_some_and(|id| {
176 id.is_empty() || id.len() > 256 || id.chars().any(char::is_control)
177 }))
178 {
179 return Err(invalid("invalid original manifest authority"));
180 }
181 Ok(())
182 }
183}
184#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
185#[serde(deny_unknown_fields)]
186pub struct OriginalPublicationManifest {
187 pub version: u16,
188 pub entries: Vec<OriginalManifestEntry>,
189}
190impl OriginalPublicationManifest {
191 pub fn new(mut entries: Vec<OriginalManifestEntry>) -> Result<Self> {
192 if entries.len() > MAX_RECORDS {
193 return Err(invalid("original manifest record bound exceeded"));
194 }
195 entries.sort_by(|a, b| a.subject.cmp(&b.subject));
196 let value = Self {
197 version: 1,
198 entries,
199 };
200 value.encode()?;
201 Ok(value)
202 }
203 pub fn encode(&self) -> Result<Vec<u8>> {
204 if self.version != 1 || self.entries.is_empty() || self.entries.len() > MAX_RECORDS {
205 return Err(invalid("original manifest record bound exceeded"));
206 }
207 let mut ids = BTreeSet::new();
208 for entry in &self.entries {
209 entry.validate()?;
210 let domain = match entry.subject {
212 ManifestSubject::Genesis(_) => 0,
213 ManifestSubject::Source(_) | ManifestSubject::OtherOperation(_) => 1,
214 ManifestSubject::OwnershipClaim(_) => 2,
215 ManifestSubject::OwnershipResolution(_) => 3,
216 };
217 if !ids.insert((domain, entry.subject.id())) {
218 return Err(invalid("duplicate original manifest identity"));
219 }
220 }
221 if self
222 .entries
223 .windows(2)
224 .any(|pair| pair[0].subject >= pair[1].subject)
225 {
226 return Err(invalid("original manifest is not sorted"));
227 }
228 let bytes = rmp_serde::to_vec_named(self)?;
229 if bytes.len() > MAX_MANIFEST_BYTES {
230 return Err(invalid("original manifest byte bound exceeded"));
231 }
232 Ok(bytes)
233 }
234 pub fn decode(bytes: &[u8]) -> Result<Self> {
235 if bytes.is_empty() || bytes.len() > MAX_MANIFEST_BYTES {
236 return Err(invalid("original manifest byte bound exceeded"));
237 }
238 let value: Self =
239 preflight::decode(bytes, true, |bytes| Ok(rmp_serde::from_slice(bytes)?))?;
240 if value.encode()? != bytes {
241 return Err(invalid("noncanonical original manifest"));
242 }
243 Ok(value)
244 }
245 pub fn id(&self) -> Result<ContentHash> {
246 Ok(ContentHash::compute_typed(MANIFEST_FORMAT, &self.encode()?))
247 }
248}
249#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
250pub enum BoundaryOriginalKind {
251 Source,
252 AccountGenesis,
253 OwnershipClaim,
254 OwnershipResolution,
255}
256#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
257#[serde(deny_unknown_fields)]
258pub struct PublicationIntent {
259 pub spool: Uuid,
260 pub spool_genesis: ContentHash,
261 pub thread: ContentHash,
262 pub revision: StateId,
263 pub inventory: ContentHash,
265 pub sharing_policy: Option<ContentHash>,
266 pub source: [u8; 32],
267 pub destination: [u8; 32],
268 pub client_operation_id: Uuid,
269}
270impl PublicationIntent {
271 pub fn encode(&self) -> Result<Vec<u8>> {
272 if self.spool.is_nil()
273 || self.client_operation_id.is_nil()
274 || self.source == [0; 32]
275 || self.destination == [0; 32]
276 {
277 return Err(invalid("invalid boundary publication intent"));
278 }
279 Ok(rmp_serde::to_vec_named(self)?)
280 }
281 pub fn decode(bytes: &[u8]) -> Result<Self> {
282 if bytes.is_empty() || bytes.len() > MAX_ACCEPTANCE_BYTES {
283 return Err(invalid("publication intent byte bound exceeded"));
284 }
285 let value: Self =
286 preflight::decode(bytes, false, |bytes| Ok(rmp_serde::from_slice(bytes)?))?;
287 if value.encode()? != bytes {
288 return Err(invalid("noncanonical publication intent"));
289 }
290 Ok(value)
291 }
292 pub fn id(&self) -> Result<ContentHash> {
293 Ok(ContentHash::compute_typed(INTENT_FORMAT, &self.encode()?))
294 }
295}
296#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
297#[serde(deny_unknown_fields)]
298pub struct OriginalBoundaryAcceptance {
299 pub version: u16,
300 pub publication_intent: ContentHash,
301 pub originals_manifest: ContentHash,
302 pub original_account: Uuid,
303 pub kinds: BTreeSet<BoundaryOriginalKind>,
304 pub accepting_publisher: [u8; 32],
305 pub accepting_author: SourceAuthor,
307}
308impl OriginalBoundaryAcceptance {
309 pub fn encode(&self) -> Result<Vec<u8>> {
310 self.accepting_author.validate()?;
311 let SourceAuthor::Account { actor, .. } = &self.accepting_author else {
312 return Err(invalid(
313 "boundary acceptance requires explicit account authority",
314 ));
315 };
316 if self.version != 1
317 || self.kinds.is_empty()
318 || self.original_account.is_nil()
319 || actor.principal_id != self.original_account
320 || self.accepting_publisher == [0; 32]
321 {
322 return Err(invalid("invalid boundary acceptance identity"));
323 }
324 let bytes = rmp_serde::to_vec_named(self)?;
325 if bytes.len() > MAX_ACCEPTANCE_BYTES {
326 return Err(invalid("boundary acceptance byte bound exceeded"));
327 }
328 Ok(bytes)
329 }
330 pub fn decode(bytes: &[u8]) -> Result<Self> {
331 if bytes.is_empty() || bytes.len() > MAX_ACCEPTANCE_BYTES {
332 return Err(invalid("boundary acceptance byte bound exceeded"));
333 }
334 let value: Self =
335 preflight::decode(bytes, false, |bytes| Ok(rmp_serde::from_slice(bytes)?))?;
336 if value.encode()? != bytes {
337 return Err(invalid("noncanonical boundary acceptance"));
338 }
339 Ok(value)
340 }
341 pub fn id(&self) -> Result<ContentHash> {
342 Ok(ContentHash::compute_typed(FORMAT, &self.encode()?))
343 }
344 pub fn selected<'a>(
347 &self,
348 intent: &PublicationIntent,
349 manifest: &'a OriginalPublicationManifest,
350 ) -> Result<Vec<&'a OriginalManifestEntry>> {
351 self.encode()?;
352 let SourceAuthor::Account { spool, .. } = &self.accepting_author else {
353 return Err(invalid("account acceptance required"));
354 };
355 if *spool != intent.spool
356 || self.publication_intent != intent.id()?
357 || self.originals_manifest != manifest.id()?
358 {
359 return Err(invalid(
360 "boundary acceptance differs from exact publication",
361 ));
362 }
363 let selected: Vec<_> = manifest
364 .entries
365 .iter()
366 .filter(|entry| {
367 let Some(authority) = &entry.authority else {
368 return false;
369 };
370 if authority.spool != intent.spool
371 || authority.actor.principal_id != self.original_account
372 {
373 return false;
374 }
375 let kind = match entry.subject {
376 ManifestSubject::Genesis(_) => BoundaryOriginalKind::AccountGenesis,
377 ManifestSubject::Source(_) => BoundaryOriginalKind::Source,
378 ManifestSubject::OwnershipClaim(_) => BoundaryOriginalKind::OwnershipClaim,
379 ManifestSubject::OwnershipResolution(_) => {
380 BoundaryOriginalKind::OwnershipResolution
381 }
382 ManifestSubject::OtherOperation(_) => return false,
383 };
384 self.kinds.contains(&kind)
385 })
386 .collect();
387 if selected.is_empty() {
388 return Err(invalid("boundary acceptance selects no original"));
389 }
390 Ok(selected)
391 }
392}
393#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
396pub enum AdmissionBasis {
397 OriginalAuthority,
398 BoundaryAcceptance { acceptance: ContentHash },
399}
400impl AdmissionBasis {
401 pub fn authorize_evidence(
404 &self,
405 evidence: Option<&OriginalBoundaryAcceptance>,
406 spool: Uuid,
407 account: Uuid,
408 kind: Option<BoundaryOriginalKind>,
409 ) -> Result<()> {
410 match (self, evidence) {
411 (Self::OriginalAuthority, None) => Ok(()),
412 (Self::BoundaryAcceptance { acceptance }, Some(value)) => {
413 let SourceAuthor::Account {
414 spool: accepting_spool,
415 ..
416 } = &value.accepting_author
417 else {
418 return Err(invalid("boundary receipt requires account acceptance"));
419 };
420 if value.id()? != *acceptance
421 || *accepting_spool != spool
422 || value.original_account != account
423 || !kind.is_some_and(|kind| value.kinds.contains(&kind))
424 {
425 return Err(invalid(
426 "boundary receipt evidence differs from original authority scope",
427 ));
428 }
429 Ok(())
430 }
431 _ => Err(invalid(
432 "receipt requires exactly its matched admission basis evidence",
433 )),
434 }
435 }
436}
437fn invalid(message: &str) -> HeddleError {
438 HeddleError::InvalidObject(message.into())
439}
440
441#[cfg(test)]
442#[path = "original_boundary_acceptance_tests.rs"]
443mod tests;