Skip to main content

auths_transparency/
entry.rs

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/// The type of mutation recorded in a transparency log entry.
9#[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/// Access tier controlling rate limits and feature gates.
31#[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    /// Returns the tier as a lowercase string matching the serde serialization.
44    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/// The body of a log entry, specific to each [`EntryType`].
55///
56/// Designed so adding new entry types in future epics is a mechanical
57/// addition of a new variant.
58#[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/// The subset of an [`Entry`] that the actor signs.
121///
122/// The sequencer assigns `sequence` and `timestamp` after — those fields
123/// are authenticated by the Merkle tree, not the actor's signature.
124///
125/// Usage:
126/// ```ignore
127/// let content = EntryContent {
128///     entry_type: EntryType::Register,
129///     body: EntryBody::Register { inception_event: serde_json::json!({}) },
130///     actor_did: CanonicalDid::parse("did:keri:E...")?,
131/// };
132/// let canonical = content.canonicalize()?;
133/// ```
134#[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    /// Canonical JSON bytes for signing (via `json-canon`).
144    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/// A complete log entry with sequencer-assigned fields.
153///
154/// Args:
155/// * `sequence` — Monotonically increasing index assigned by the sequencer.
156/// * `timestamp` — Wall-clock time assigned by the sequencer.
157/// * `content` — The actor-signed payload.
158/// * `actor_sig` — Ed25519 signature over the canonical `EntryContent`.
159///
160/// Usage:
161/// ```ignore
162/// let entry = Entry {
163///     sequence: 0,
164///     timestamp: Utc::now(),
165///     content: entry_content,
166///     actor_sig: sig,
167/// };
168/// let leaf_data = entry.leaf_data()?;
169/// ```
170#[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    /// Canonical JSON bytes of the full entry for Merkle leaf hashing.
181    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}