1use chrono::{DateTime, Utc};
2use sentinel_crypto::{hash_data, sign_hash, SigningKey};
3use serde_json::Value;
4use tracing::{debug, trace};
5
6use crate::Result;
7
8#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone, PartialEq, Eq)]
10#[allow(
11 clippy::field_scoped_visibility_modifiers,
12 reason = "fields need to be pub(crate) for internal access"
13)]
14pub struct Document {
15 pub(crate) id: String,
17 pub(crate) version: u32,
19 pub(crate) created_at: DateTime<Utc>,
21 pub(crate) updated_at: DateTime<Utc>,
23 pub(crate) hash: String,
25 pub(crate) signature: String,
27 pub(crate) data: Value,
29}
30
31impl Document {
32 pub async fn new(id: String, data: Value, private_key: &SigningKey) -> Result<Self> {
35 trace!("Creating new signed document with id: {}", id);
36 let now = Utc::now();
37 let hash = hash_data(&data).await?;
38 let signature = sign_hash(&hash, private_key).await?;
39 debug!("Document {} created with hash: {}", id, hash);
40 Ok(Self {
41 id,
42 version: crate::META_SENTINEL_VERSION,
43 created_at: now,
44 updated_at: now,
45 hash,
46 signature,
47 data,
48 })
49 }
50
51 pub async fn new_without_signature(id: String, data: Value) -> Result<Self> {
54 trace!("Creating new unsigned document with id: {}", id);
55 let now = Utc::now();
56 let hash = hash_data(&data).await?;
57 debug!("Document {} created without signature, hash: {}", id, hash);
58 Ok(Self {
59 id,
60 version: crate::META_SENTINEL_VERSION,
61 created_at: now,
62 updated_at: now,
63 hash,
64 signature: String::new(),
65 data,
66 })
67 }
68
69 pub fn id(&self) -> &str { &self.id }
71
72 pub const fn version(&self) -> u32 { self.version }
74
75 pub const fn created_at(&self) -> DateTime<Utc> { self.created_at }
77
78 pub const fn updated_at(&self) -> DateTime<Utc> { self.updated_at }
80
81 pub fn hash(&self) -> &str { &self.hash }
83
84 pub fn signature(&self) -> &str { &self.signature }
86
87 pub const fn data(&self) -> &Value { &self.data }
89
90 pub async fn set_data(&mut self, data: Value, private_key: &SigningKey) -> Result<()> {
93 trace!("Updating data for document: {}", self.id);
94 self.data = data;
95 self.updated_at = Utc::now();
96 self.hash = hash_data(&self.data).await?;
97 self.signature = sign_hash(&self.hash, private_key).await?;
98 debug!("Document {} data updated, new hash: {}", self.id, self.hash);
99 Ok(())
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use rand::{rngs::OsRng, RngCore};
106 use sentinel_crypto::SigningKey;
107
108 use super::*;
109
110 #[tokio::test]
111 async fn test_document_creation() {
112 let mut rng = OsRng;
113 let mut key_bytes = [0u8; 32];
114 rng.fill_bytes(&mut key_bytes);
115 let private_key = SigningKey::from_bytes(&key_bytes);
116 let data = serde_json::json!({"name": "Test", "value": 42});
117 let doc = Document::new("test-id".to_string(), data.clone(), &private_key)
118 .await
119 .unwrap();
120
121 assert_eq!(doc.id(), "test-id");
122 assert_eq!(doc.version(), crate::META_SENTINEL_VERSION);
123 assert_eq!(doc.data(), &data);
124 assert!(!doc.hash().is_empty());
125 assert!(!doc.signature().is_empty());
126 assert_eq!(doc.created_at(), doc.updated_at());
127 }
128
129 #[tokio::test]
130 async fn test_document_with_empty_data() {
131 let mut rng = OsRng;
132 let mut key_bytes = [0u8; 32];
133 rng.fill_bytes(&mut key_bytes);
134 let private_key = SigningKey::from_bytes(&key_bytes);
135 let data = serde_json::json!({});
136 let doc = Document::new("empty".to_string(), data.clone(), &private_key)
137 .await
138 .unwrap();
139
140 assert_eq!(doc.id(), "empty");
141 assert_eq!(doc.version(), crate::META_SENTINEL_VERSION);
142 assert!(doc.data().as_object().unwrap().is_empty());
143 }
144
145 #[tokio::test]
146 async fn test_document_with_complex_data() {
147 let mut rng = OsRng;
148 let mut key_bytes = [0u8; 32];
149 rng.fill_bytes(&mut key_bytes);
150 let private_key = SigningKey::from_bytes(&key_bytes);
151 let data = serde_json::json!({
152 "string": "value",
153 "number": 123,
154 "boolean": true,
155 "array": [1, 2, 3],
156 "object": {"nested": "value"}
157 });
158 let doc = Document::new("complex".to_string(), data.clone(), &private_key)
159 .await
160 .unwrap();
161
162 assert_eq!(doc.data()["string"], "value");
163 assert_eq!(doc.data()["number"], 123);
164 assert_eq!(doc.data()["boolean"], true);
165 assert_eq!(doc.data()["array"], serde_json::json!([1, 2, 3]));
166 assert_eq!(doc.data()["object"]["nested"], "value");
167 }
168
169 #[tokio::test]
170 async fn test_document_with_valid_filename_safe_ids() {
171 let mut rng = OsRng;
172 let mut key_bytes = [0u8; 32];
173 rng.fill_bytes(&mut key_bytes);
174 let private_key = SigningKey::from_bytes(&key_bytes);
175 let valid_ids = vec![
177 "user-123",
178 "user_456",
179 "user123",
180 "123",
181 "a",
182 "user-123_test",
183 "CamelCaseID",
184 ];
185
186 for id in valid_ids {
187 let data = serde_json::json!({"data": "test"});
188 let doc = Document::new(id.to_owned(), data.clone(), &private_key)
189 .await
190 .unwrap();
191
192 assert_eq!(doc.id(), id);
193 assert_eq!(doc.data(), &data);
194 }
195 }
196
197 #[tokio::test]
198 async fn test_set_data_updates_hash_and_signature() {
199 let mut rng = OsRng;
200 let mut key_bytes = [0u8; 32];
201 rng.fill_bytes(&mut key_bytes);
202 let private_key = SigningKey::from_bytes(&key_bytes);
203 let initial_data = serde_json::json!({"initial": "data"});
204 let mut doc = Document::new("test".to_string(), initial_data, &private_key)
205 .await
206 .unwrap();
207 let initial_hash = doc.hash().to_string();
208 let initial_signature = doc.signature().to_string();
209 let initial_updated_at = doc.updated_at();
210
211 let new_data = serde_json::json!({"new": "data"});
212 doc.set_data(new_data.clone(), &private_key).await.unwrap();
213
214 assert_eq!(doc.data(), &new_data);
215 assert_ne!(doc.hash(), initial_hash);
216 assert_ne!(doc.signature(), initial_signature);
217 assert!(doc.updated_at() > initial_updated_at);
218 }
219}