#[cfg(feature = "host")]
pub mod audit;
#[cfg(feature = "host")]
pub mod authz;
pub mod error;
pub mod lifecycle;
pub mod merkle;
#[cfg(feature = "mls")]
pub mod mls;
#[cfg(feature = "mls")]
pub mod retention;
#[cfg(feature = "mls")]
pub mod sealed;
#[cfg(feature = "host")]
pub mod storage;
pub mod wire;
use serde::{Deserialize, Serialize};
pub const ROOMS_KEYSPACE: &str = "rooms";
pub const ROOM_RECORDS_KEYSPACE: &str = "room_records";
pub const ROOM_EPOCH_LINKS_KEYSPACE: &str = "room_epoch_links";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RetentionPolicy {
#[default]
Chained,
FromJoin,
}
impl RetentionPolicy {
pub fn links_epochs(&self) -> bool {
matches!(self, RetentionPolicy::Chained)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Visibility {
Open,
Attributed,
Private,
}
impl Visibility {
pub fn stores_cleartext(&self) -> bool {
matches!(self, Visibility::Open)
}
pub fn discloses_actor(&self) -> bool {
matches!(self, Visibility::Open | Visibility::Attributed)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Room {
pub room_id: String,
pub owner_did: String,
pub visibility: Visibility,
#[serde(default)]
pub anchor_cadence: AnchorCadence,
#[serde(default)]
pub retention_policy: RetentionPolicy,
pub epoch: u32,
pub next_version: u64,
pub retention_days: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub epoch_expires_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mirror_of: Option<String>,
pub created_at: u64,
pub updated_at: u64,
}
impl Room {
pub fn is_mirror(&self) -> bool {
self.mirror_of.is_some()
}
pub fn watermark(&self) -> u64 {
self.next_version.saturating_sub(1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnchorCadence {
Never,
Renewal,
#[default]
Manual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RecordStatus {
Active,
Deprecated,
Retracted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Record {
pub key: String,
pub version: u64,
pub epoch: Option<u32>,
pub status: RecordStatus,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub pinned: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub sealed: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cleartext: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
pub updated_at: u64,
}
fn rfc3339(unix_seconds: u64) -> String {
chrono::DateTime::from_timestamp(unix_seconds as i64, 0)
.unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).expect("epoch is in range"))
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}
impl Record {
pub fn metadata(&self) -> serde_json::Value {
let mut map = serde_json::Map::new();
map.insert("key".into(), serde_json::json!(self.key));
map.insert("version".into(), serde_json::json!(self.version));
map.insert("status".into(), serde_json::json!(self.status));
map.insert(
"updatedAt".into(),
serde_json::json!(rfc3339(self.updated_at)),
);
if let Some(epoch) = self.epoch {
map.insert("epoch".into(), serde_json::json!(epoch));
}
if let Some(author) = &self.author {
map.insert("author".into(), serde_json::json!(author));
}
if let Some(cleartext) = &self.cleartext {
for field in ["title", "description"] {
if let Some(v) = cleartext.get(field).filter(|v| v.is_string()) {
map.insert(field.into(), v.clone());
}
}
}
serde_json::Value::Object(map)
}
#[must_use]
pub fn committed(&self) -> wire::CommittedRecord {
let sealed = match (&self.sealed, &self.nonce, self.epoch) {
(Some(ciphertext), Some(nonce), Some(epoch)) => Some(wire::SealedContent {
ciphertext: ciphertext.clone(),
nonce: nonce.clone(),
epoch,
}),
_ => None,
};
wire::CommittedRecord {
key: self.key.clone(),
version: self.version,
status: self.status,
updated_at: rfc3339(self.updated_at),
pinned: self.pinned,
author: self.author.clone(),
sealed,
cleartext: self.cleartext.clone(),
}
}
pub fn from_wire(record: &wire::CommittedRecord) -> Result<Self, FromWireError> {
let parsed = chrono::DateTime::parse_from_rfc3339(&record.updated_at).map_err(|_| {
FromWireError::UpdatedAt {
key: record.key.clone(),
value: record.updated_at.clone(),
}
})?;
let updated_at =
u64::try_from(parsed.timestamp()).map_err(|_| FromWireError::UpdatedAt {
key: record.key.clone(),
value: record.updated_at.clone(),
})?;
Ok(Self {
key: record.key.clone(),
version: record.version,
epoch: record.sealed.as_ref().map(|s| s.epoch),
status: record.status,
pinned: record.pinned,
sealed: record.sealed.as_ref().map(|s| s.ciphertext.clone()),
nonce: record.sealed.as_ref().map(|s| s.nonce.clone()),
cleartext: record.cleartext.clone(),
author: record.author.clone(),
updated_at,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum FromWireError {
#[error(
"record `{key}` carries `updatedAt` = `{value}`, which is not a storable RFC 3339 timestamp"
)]
UpdatedAt { key: String, value: String },
}
#[cfg(test)]
mod retention_policy_tests {
use super::*;
#[test]
fn a_room_stored_before_the_chain_existed_chains_from_here() {
let json = r#"{
"roomId": "did:webvh:example.com:rooms:legacy",
"ownerDid": "did:key:zOwner",
"visibility": "attributed",
"epoch": 4,
"nextVersion": 9,
"retentionDays": 90,
"createdAt": 0,
"updatedAt": 0
}"#;
let room: Room = serde_json::from_str(json).expect("a pre-chain room deserialises");
assert_eq!(room.retention_policy, RetentionPolicy::Chained);
assert!(
room.retention_policy.links_epochs(),
"a legacy room must be able to chain from here, whatever it lost before"
);
}
#[test]
fn only_a_chained_room_accepts_rungs() {
assert!(RetentionPolicy::Chained.links_epochs());
assert!(!RetentionPolicy::FromJoin.links_epochs());
}
}