1use chrono::{DateTime, Utc};
2use serde::{Serialize, Deserialize};
3
4use crate::crypto::ContentHash;
5
6#[derive(Clone, Debug, Serialize, Deserialize)]
8pub struct TibetToken {
9 pub token_id: String,
10 pub parent_id: Option<String>,
11 pub timestamp: DateTime<Utc>,
12
13 pub erin: ContentHash,
15
16 pub eraan: Vec<String>,
18
19 pub eromheen: Eromheen,
21
22 pub erachter: String,
24
25 pub signature: Option<Vec<u8>>,
27}
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
31pub struct Eromheen {
32 pub actor: String,
33 pub jis_level: u8,
34 pub chunks_accessed: usize,
35 pub chunks_denied: usize,
36 pub airlock_session_ms: Option<f64>,
37}
38
39#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct Provenance {
42 pub chain: Vec<TibetToken>,
43}
44
45impl TibetToken {
46 pub fn new(
47 erin: ContentHash,
48 erachter: impl Into<String>,
49 actor: impl Into<String>,
50 jis_level: u8,
51 ) -> Self {
52 let id = format!("tibet_{}", Utc::now().timestamp_nanos_opt().unwrap_or(0));
53 Self {
54 token_id: id,
55 parent_id: None,
56 timestamp: Utc::now(),
57 erin,
58 eraan: Vec::new(),
59 eromheen: Eromheen {
60 actor: actor.into(),
61 jis_level,
62 chunks_accessed: 0,
63 chunks_denied: 0,
64 airlock_session_ms: None,
65 },
66 erachter: erachter.into(),
67 signature: None,
68 }
69 }
70
71 pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
72 self.parent_id = Some(parent_id.into());
73 self
74 }
75
76 pub fn with_access_stats(mut self, accessed: usize, denied: usize) -> Self {
77 self.eromheen.chunks_accessed = accessed;
78 self.eromheen.chunks_denied = denied;
79 self
80 }
81
82 pub fn with_airlock_time(mut self, ms: f64) -> Self {
83 self.eromheen.airlock_session_ms = Some(ms);
84 self
85 }
86
87 pub fn signable_bytes(&self) -> Vec<u8> {
89 let mut token = self.clone();
90 token.signature = None;
91 serde_json::to_vec(&token).unwrap_or_default()
92 }
93}
94
95impl Provenance {
96 pub fn new() -> Self {
97 Self { chain: Vec::new() }
98 }
99
100 pub fn append(&mut self, token: TibetToken) {
101 self.chain.push(token);
102 }
103
104 pub fn latest(&self) -> Option<&TibetToken> {
105 self.chain.last()
106 }
107
108 pub fn len(&self) -> usize {
109 self.chain.len()
110 }
111
112 pub fn is_empty(&self) -> bool {
113 self.chain.is_empty()
114 }
115
116 pub fn verify_chain(&self) -> bool {
118 for window in self.chain.windows(2) {
119 let parent = &window[0];
120 let child = &window[1];
121 match &child.parent_id {
122 Some(pid) if pid == &parent.token_id => continue,
123 _ => return false,
124 }
125 }
126 true
127 }
128}
129
130impl Default for Provenance {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_token_creation() {
142 let hash = ContentHash("sha256:abc123".into());
143 let token = TibetToken::new(hash, "test query", "user@test.com", 2);
144 assert!(token.token_id.starts_with("tibet_"));
145 assert_eq!(token.eromheen.jis_level, 2);
146 assert_eq!(token.erachter, "test query");
147 }
148
149 #[test]
150 fn test_provenance_chain() {
151 let h1 = ContentHash("sha256:aaa".into());
152 let h2 = ContentHash("sha256:bbb".into());
153
154 let t1 = TibetToken::new(h1, "first", "actor", 0);
155 let t1_id = t1.token_id.clone();
156 let t2 = TibetToken::new(h2, "second", "actor", 0).with_parent(t1_id);
157
158 let mut prov = Provenance::new();
159 prov.append(t1);
160 prov.append(t2);
161
162 assert_eq!(prov.len(), 2);
163 assert!(prov.verify_chain());
164 }
165
166 #[test]
167 fn test_broken_chain() {
168 let h1 = ContentHash("sha256:aaa".into());
169 let h2 = ContentHash("sha256:bbb".into());
170
171 let t1 = TibetToken::new(h1, "first", "actor", 0);
172 let t2 = TibetToken::new(h2, "second", "actor", 0).with_parent("wrong_parent");
173
174 let mut prov = Provenance::new();
175 prov.append(t1);
176 prov.append(t2);
177
178 assert!(!prov.verify_chain());
179 }
180}