use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::{Capture, metadata::AUTHORITY_FORMAT};
use crate::{
error::{HeddleError, Result},
object::{CollaborationActor, ContentHash},
};
pub const MAX_SOURCE_AUTHORITY_BYTES: usize = 64 * 1024;
pub const SOURCE_AUTHORIZATION_METHOD: &str = "/heddle.api.v1alpha2.SyncService/PublishContent";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum SourceAuthor {
LocalKey,
Account {
spool: Uuid,
actor: CollaborationActor,
authority_digest: ContentHash,
#[serde(with = "serde_bytes")]
authority: Vec<u8>,
},
}
impl SourceAuthor {
pub fn account(spool: Uuid, actor: CollaborationActor, authority: Vec<u8>) -> Result<Self> {
let value = Self::Account {
spool,
actor,
authority_digest: ContentHash::compute_typed(AUTHORITY_FORMAT, &authority),
authority,
};
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<()> {
if let Self::Account {
spool,
actor,
authority_digest,
authority,
} = self
{
if spool.is_nil()
|| actor.principal_id.is_nil()
|| actor.agent_id.as_ref().is_some_and(|id| {
id.is_empty() || id.len() > 256 || id.chars().any(char::is_control)
})
{
return Err(invalid("invalid original source author identity"));
}
if authority.is_empty()
|| authority.len() > MAX_SOURCE_AUTHORITY_BYTES
|| ContentHash::compute_typed(AUTHORITY_FORMAT, authority) != *authority_digest
{
return Err(invalid("invalid original source authority binding"));
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthoredCapture {
pub result: Capture,
pub author: SourceAuthor,
}
impl AuthoredCapture {
pub fn local(result: Capture) -> Self {
Self {
result,
author: SourceAuthor::LocalKey,
}
}
pub fn account(
result: Capture,
spool: Uuid,
actor: CollaborationActor,
authority: Vec<u8>,
) -> Result<Self> {
Ok(Self {
result,
author: SourceAuthor::account(spool, actor, authority)?,
})
}
}
fn invalid(message: &str) -> HeddleError {
HeddleError::InvalidObject(message.into())
}
#[cfg(test)]
mod tests {
use super::*;
fn actor() -> CollaborationActor {
CollaborationActor {
principal_id: Uuid::from_u128(1),
agent_id: Some("capture-agent".into()),
}
}
#[test]
fn original_source_authority_binds_exact_envelope_and_bounded_identity() {
let author = SourceAuthor::account(Uuid::from_u128(2), actor(), vec![3; 32])
.expect("bounded authority");
let SourceAuthor::Account {
authority_digest, ..
} = &author
else {
panic!("account author")
};
assert_eq!(
*authority_digest,
ContentHash::compute_typed(AUTHORITY_FORMAT, &[3; 32])
);
let mut changed = author;
let SourceAuthor::Account { authority, .. } = &mut changed else {
panic!("account author")
};
authority[0] ^= 1;
assert!(
changed.validate().is_err(),
"authority bytes cannot change under original digest"
);
assert!(SourceAuthor::account(Uuid::nil(), actor(), vec![1]).is_err());
assert!(SourceAuthor::account(Uuid::from_u128(2), actor(), vec![]).is_err());
assert!(
SourceAuthor::account(
Uuid::from_u128(2),
actor(),
vec![1; MAX_SOURCE_AUTHORITY_BYTES + 1]
)
.is_err()
);
let mut invalid_agent = actor();
invalid_agent.agent_id = Some("\n".into());
assert!(SourceAuthor::account(Uuid::from_u128(2), invalid_agent, vec![1]).is_err());
SourceAuthor::LocalKey
.validate()
.expect("local key needs no account enrollment");
}
}