1use auths_verifier::{
2 CanonicalDid, Capability, Ed25519PublicKey, Ed25519Signature, IdentityDID, Role,
3};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11#[non_exhaustive]
12#[allow(missing_docs)]
13pub enum EntryType {
14 Register,
15 Rotate,
16 Abandon,
17 OrgCreate,
18 OrgAddMember,
19 OrgRevokeMember,
20 DeviceBind,
21 DeviceRevoke,
22 Attest,
23 NamespaceClaim,
24 NamespaceDelegate,
25 NamespaceTransfer,
26 AccessGrant,
27 AccessRevoke,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33#[non_exhaustive]
34#[allow(missing_docs)]
35pub enum AccessTier {
36 Anonymous,
37 Free,
38 Team,
39 Enterprise,
40}
41
42impl AccessTier {
43 pub fn as_str(&self) -> &'static str {
45 match self {
46 Self::Anonymous => "anonymous",
47 Self::Free => "free",
48 Self::Team => "team",
49 Self::Enterprise => "enterprise",
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[serde(tag = "type", rename_all = "snake_case")]
60#[non_exhaustive]
61#[allow(missing_docs)]
62pub enum EntryBody {
63 Register {
64 inception_event: Value,
65 },
66 Rotate {
67 rotation_event: Value,
68 },
69 Abandon {
70 reason: Option<String>,
71 },
72 OrgCreate {
73 display_name: String,
74 },
75 OrgAddMember {
76 member_did: IdentityDID,
77 role: Role,
78 capabilities: Vec<Capability>,
79 delegated_by: IdentityDID,
80 },
81 OrgRevokeMember {
82 member_did: IdentityDID,
83 },
84 DeviceBind {
85 device_did: CanonicalDid,
86 public_key: Ed25519PublicKey,
87 },
88 DeviceRevoke {
89 device_did: CanonicalDid,
90 },
91 Attest(Value),
92 NamespaceClaim {
93 ecosystem: String,
94 package_name: String,
95 proof_url: String,
96 verification_method: String,
97 },
98 NamespaceDelegate {
99 ecosystem: String,
100 package_name: String,
101 delegate_did: IdentityDID,
102 },
103 NamespaceTransfer {
104 ecosystem: String,
105 package_name: String,
106 new_owner_did: IdentityDID,
107 },
108 AccessGrant {
109 subject_did: IdentityDID,
110 tier: AccessTier,
111 daily_limit: u32,
112 expires_at: DateTime<Utc>,
113 },
114 AccessRevoke {
115 subject_did: IdentityDID,
116 reason: Option<String>,
117 },
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135#[allow(missing_docs)]
136pub struct EntryContent {
137 pub entry_type: EntryType,
138 pub body: EntryBody,
139 pub actor_did: CanonicalDid,
140}
141
142impl EntryContent {
143 pub fn canonicalize(&self) -> Result<Vec<u8>, crate::error::TransparencyError> {
145 let value = serde_json::to_value(self)
146 .map_err(|e| crate::error::TransparencyError::EntryError(e.to_string()))?;
147 json_canon::to_vec(&value)
148 .map_err(|e| crate::error::TransparencyError::EntryError(e.to_string()))
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171#[allow(missing_docs)]
172pub struct Entry {
173 pub sequence: u128,
174 pub timestamp: DateTime<Utc>,
175 pub content: EntryContent,
176 pub actor_sig: Ed25519Signature,
177}
178
179impl Entry {
180 pub fn leaf_data(&self) -> Result<Vec<u8>, crate::error::TransparencyError> {
182 let value = serde_json::to_value(self)
183 .map_err(|e| crate::error::TransparencyError::EntryError(e.to_string()))?;
184 json_canon::to_vec(&value)
185 .map_err(|e| crate::error::TransparencyError::EntryError(e.to_string()))
186 }
187}
188
189#[cfg(test)]
190#[allow(clippy::disallowed_methods)]
191mod tests {
192 use super::*;
193 use auths_verifier::Ed25519Signature;
194
195 #[test]
196 fn entry_type_serializes_snake_case() {
197 let json = serde_json::to_string(&EntryType::Register).unwrap();
198 assert_eq!(json, r#""register""#);
199 }
200
201 #[test]
202 fn entry_content_canonicalize_deterministic() {
203 let content = EntryContent {
204 entry_type: EntryType::DeviceBind,
205 body: EntryBody::DeviceBind {
206 device_did: CanonicalDid::new_unchecked(
207 "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
208 ),
209 public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
210 },
211 actor_did: CanonicalDid::new_unchecked("did:keri:Eabc"),
212 };
213 let a = content.canonicalize().unwrap();
214 let b = content.canonicalize().unwrap();
215 assert_eq!(a, b);
216 }
217
218 #[test]
219 fn entry_json_roundtrip() {
220 let entry = Entry {
221 sequence: 42,
222 timestamp: chrono::DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")
223 .unwrap()
224 .with_timezone(&Utc),
225 content: EntryContent {
226 entry_type: EntryType::OrgAddMember,
227 body: EntryBody::OrgAddMember {
228 member_did: IdentityDID::new_unchecked("did:keri:Emember"),
229 role: Role::Admin,
230 capabilities: vec![Capability::sign_commit()],
231 delegated_by: IdentityDID::new_unchecked("did:keri:Eadmin"),
232 },
233 actor_did: CanonicalDid::new_unchecked("did:keri:Eadmin"),
234 },
235 actor_sig: Ed25519Signature::empty(),
236 };
237 let json = serde_json::to_string(&entry).unwrap();
238 let back: Entry = serde_json::from_str(&json).unwrap();
239 assert_eq!(entry.sequence, back.sequence);
240 }
241
242 #[test]
243 fn namespace_claim_with_proof_roundtrips() {
244 let body = EntryBody::NamespaceClaim {
245 ecosystem: "npm".to_string(),
246 package_name: "left-pad".to_string(),
247 proof_url: "https://registry.npmjs.org/left-pad".to_string(),
248 verification_method: "ApiOwnership".to_string(),
249 };
250 let json = serde_json::to_string(&body).unwrap();
251 assert!(json.contains("proof_url"));
252 assert!(json.contains("verification_method"));
253 let back: EntryBody = serde_json::from_str(&json).unwrap();
254 assert_eq!(body, back);
255 }
256}