use auths_verifier::{
CanonicalDid, Capability, Ed25519PublicKey, Ed25519Signature, IdentityDID, Role,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum EntryType {
Register,
Rotate,
Abandon,
OrgCreate,
OrgAddMember,
OrgRevokeMember,
DeviceBind,
DeviceRevoke,
Attest,
NamespaceClaim,
NamespaceDelegate,
NamespaceTransfer,
AccessGrant,
AccessRevoke,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum AccessTier {
Anonymous,
Free,
Team,
Enterprise,
}
impl AccessTier {
pub fn as_str(&self) -> &'static str {
match self {
Self::Anonymous => "anonymous",
Self::Free => "free",
Self::Team => "team",
Self::Enterprise => "enterprise",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum EntryBody {
Register {
inception_event: Value,
},
Rotate {
rotation_event: Value,
},
Abandon {
reason: Option<String>,
},
OrgCreate {
display_name: String,
},
OrgAddMember {
member_did: IdentityDID,
role: Role,
capabilities: Vec<Capability>,
delegated_by: IdentityDID,
},
OrgRevokeMember {
member_did: IdentityDID,
},
DeviceBind {
device_did: CanonicalDid,
public_key: Ed25519PublicKey,
},
DeviceRevoke {
device_did: CanonicalDid,
},
Attest(Value),
NamespaceClaim {
ecosystem: String,
package_name: String,
proof_url: String,
verification_method: String,
},
NamespaceDelegate {
ecosystem: String,
package_name: String,
delegate_did: IdentityDID,
},
NamespaceTransfer {
ecosystem: String,
package_name: String,
new_owner_did: IdentityDID,
},
AccessGrant {
subject_did: IdentityDID,
tier: AccessTier,
daily_limit: u32,
expires_at: DateTime<Utc>,
},
AccessRevoke {
subject_did: IdentityDID,
reason: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct EntryContent {
pub entry_type: EntryType,
pub body: EntryBody,
pub actor_did: CanonicalDid,
}
impl EntryContent {
pub fn canonicalize(&self) -> Result<Vec<u8>, crate::error::TransparencyError> {
let value = serde_json::to_value(self)
.map_err(|e| crate::error::TransparencyError::EntryError(e.to_string()))?;
json_canon::to_vec(&value)
.map_err(|e| crate::error::TransparencyError::EntryError(e.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct Entry {
pub sequence: u128,
pub timestamp: DateTime<Utc>,
pub content: EntryContent,
pub actor_sig: Ed25519Signature,
}
impl Entry {
pub fn leaf_data(&self) -> Result<Vec<u8>, crate::error::TransparencyError> {
let value = serde_json::to_value(self)
.map_err(|e| crate::error::TransparencyError::EntryError(e.to_string()))?;
json_canon::to_vec(&value)
.map_err(|e| crate::error::TransparencyError::EntryError(e.to_string()))
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use auths_verifier::Ed25519Signature;
#[test]
fn entry_type_serializes_snake_case() {
let json = serde_json::to_string(&EntryType::Register).unwrap();
assert_eq!(json, r#""register""#);
}
#[test]
fn entry_content_canonicalize_deterministic() {
let content = EntryContent {
entry_type: EntryType::DeviceBind,
body: EntryBody::DeviceBind {
device_did: CanonicalDid::new_unchecked(
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
),
public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
},
actor_did: CanonicalDid::new_unchecked("did:keri:Eabc"),
};
let a = content.canonicalize().unwrap();
let b = content.canonicalize().unwrap();
assert_eq!(a, b);
}
#[test]
fn entry_json_roundtrip() {
let entry = Entry {
sequence: 42,
timestamp: chrono::DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
content: EntryContent {
entry_type: EntryType::OrgAddMember,
body: EntryBody::OrgAddMember {
member_did: IdentityDID::new_unchecked("did:keri:Emember"),
role: Role::Admin,
capabilities: vec![Capability::sign_commit()],
delegated_by: IdentityDID::new_unchecked("did:keri:Eadmin"),
},
actor_did: CanonicalDid::new_unchecked("did:keri:Eadmin"),
},
actor_sig: Ed25519Signature::empty(),
};
let json = serde_json::to_string(&entry).unwrap();
let back: Entry = serde_json::from_str(&json).unwrap();
assert_eq!(entry.sequence, back.sequence);
}
#[test]
fn namespace_claim_with_proof_roundtrips() {
let body = EntryBody::NamespaceClaim {
ecosystem: "npm".to_string(),
package_name: "left-pad".to_string(),
proof_url: "https://registry.npmjs.org/left-pad".to_string(),
verification_method: "ApiOwnership".to_string(),
};
let json = serde_json::to_string(&body).unwrap();
assert!(json.contains("proof_url"));
assert!(json.contains("verification_method"));
let back: EntryBody = serde_json::from_str(&json).unwrap();
assert_eq!(body, back);
}
}