qualia_client_core/wellfair/
vault.rs1use std::path::{Path, PathBuf};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use ed25519_dalek::SigningKey;
5use qualia_core_db::crdt::SuspendedTransactionQueue;
6use qualia_core_db::git_bridge::DagStore;
7use qualia_core_db::wal::{commit_semantic_mutation, WalHandoffResult, WriteAheadLog};
8use qualia_core_db::NQuin;
9use wellfare_core::record::{RecordEnvelope, SensitivityClass};
10
11use super::checkpoint_store::{self, load_dag, load_meta};
12use super::consent_store::ConsentStore;
13use super::graph_store::GraphStore;
14use super::journal::{JournalEntry, WellfairJournal};
15use super::receipt::{ReceiptLog, ReceiptRecord};
16use super::sync_outbox::{SyncOutbox, SyncOutboxEntry};
17
18pub struct VaultService {
20 wal: WriteAheadLog,
21 dag: DagStore,
22 graph: GraphStore,
23 suspended: SuspendedTransactionQueue,
24 author_did: u64,
25 storage_root: PathBuf,
26 journal: WellfairJournal,
27 receipts: ReceiptLog,
28 consents: ConsentStore,
29 sync_outbox: SyncOutbox,
30 last_checkpoint_hash: Option<[u8; 32]>,
31}
32
33impl VaultService {
34 pub fn open<W: AsRef<Path>, S: AsRef<Path>>(
35 wal_path: W,
36 storage_root: S,
37 author_did: u64,
38 ) -> std::io::Result<Self> {
39 let storage_root = storage_root.as_ref().to_path_buf();
40 let wal = WriteAheadLog::open(wal_path)?;
41 let dag = load_dag(&storage_root);
42 let graph = GraphStore::open(&storage_root)?;
43 let suspended = SuspendedTransactionQueue::new();
44 let journal = WellfairJournal::open(&storage_root)?;
45 let receipts = ReceiptLog::open(&storage_root)?;
46 let consents = ConsentStore::open(&storage_root)?;
47 let sync_outbox = SyncOutbox::open(&storage_root)?;
48
49 let last_checkpoint_hash = if wal.prev_dag_hash != [0u8; 32] {
50 Some(wal.prev_dag_hash)
51 } else if let Some(meta) = load_meta(&storage_root) {
52 hex::decode(&meta.last_hash_hex)
53 .ok()
54 .and_then(|b| b.try_into().ok())
55 } else {
56 None
57 };
58
59 Ok(Self {
60 wal,
61 dag,
62 graph,
63 suspended,
64 author_did,
65 storage_root,
66 journal,
67 receipts,
68 consents,
69 sync_outbox,
70 last_checkpoint_hash,
71 })
72 }
73
74 pub fn graph_quin_count(&self) -> usize {
75 self.graph.count()
76 }
77
78 pub fn list_graph_quins(&self, limit: usize) -> std::io::Result<Vec<NQuin>> {
79 self.graph.list_recent(limit)
80 }
81
82 pub fn graph_coverage(
83 &self,
84 journal_limit: usize,
85 ) -> std::io::Result<Vec<super::graph_query::GraphCoverageRow>> {
86 let journal = self.journal.list_recent(journal_limit)?;
87 let quin_limit = journal.len().saturating_mul(8).max(64);
88 let quins = self.graph.list_recent(quin_limit)?;
89 Ok(super::graph_query::coverage_for_journal(&journal, &quins))
90 }
91
92 pub fn journal_count(&self) -> std::io::Result<usize> {
93 self.journal.count()
94 }
95
96 pub fn list_health_records(&self, limit: usize) -> std::io::Result<Vec<JournalEntry>> {
97 self.journal.list_recent(limit)
98 }
99
100 pub fn list_receipts(&self, limit: usize) -> std::io::Result<Vec<ReceiptRecord>> {
101 self.receipts.list_recent(limit)
102 }
103
104 pub fn list_outbox(&self, limit: usize) -> std::io::Result<Vec<SyncOutboxEntry>> {
105 self.sync_outbox.list_recent(limit)
106 }
107
108 pub fn outbox_queued_count(&self) -> std::io::Result<usize> {
109 self.sync_outbox.count_queued()
110 }
111
112 pub fn list_active_consents(
113 &self,
114 now_unix: u64,
115 ) -> std::io::Result<Vec<super::consent_store::ConsentGrantRecord>> {
116 self.consents.list_active(now_unix)
117 }
118
119 pub fn append_consent(
120 &self,
121 grant: &super::consent_store::ConsentGrantRecord,
122 ) -> std::io::Result<()> {
123 self.consents.append(grant)
124 }
125
126 pub fn revoke_consent(&self, grant_id: &str) -> std::io::Result<bool> {
127 self.consents.revoke(grant_id)
128 }
129
130 pub fn wal_buffered_quins(&mut self) -> std::io::Result<usize> {
131 self.wal.buffered_count()
132 }
133
134 pub fn commit_quin(
136 &mut self,
137 mut quin: NQuin,
138 signing_key: &SigningKey,
139 principal_did_hash: u64,
140 ) -> std::io::Result<WalHandoffResult> {
141 commit_semantic_mutation(
142 &mut self.wal,
143 &mut quin,
144 principal_did_hash,
145 self.author_did,
146 signing_key,
147 &mut self.suspended,
148 )
149 }
150
151 pub fn checkpoint(&mut self) -> std::io::Result<[u8; 32]> {
153 let timestamp_ms = SystemTime::now()
154 .duration_since(UNIX_EPOCH)
155 .map(|d| d.as_millis() as u64)
156 .unwrap_or(0);
157 let hash = self
158 .wal
159 .checkpoint_to_dag(&mut self.dag, self.author_did, timestamp_ms)?;
160 let committed = self.wal.recover()?;
161 if !committed.is_empty() {
162 self.graph.append_quins(&committed)?;
163 checkpoint_store::persist_checkpoint(
164 &self.storage_root,
165 &self.dag,
166 hash,
167 self.graph.count(),
168 &committed,
169 self.author_did,
170 )?;
171 } else if self.dag.nodes().len() > 0 {
172 checkpoint_store::save_dag(&self.storage_root, &self.dag)?;
173 }
174 self.wal.truncate()?;
175 self.last_checkpoint_hash = Some(hash);
176 Ok(hash)
177 }
178
179 pub fn last_checkpoint_hash(&self) -> Option<[u8; 32]> {
180 self.last_checkpoint_hash
181 }
182
183 pub fn checkpoint_meta(&self) -> Option<checkpoint_store::CheckpointMeta> {
184 load_meta(&self.storage_root)
185 }
186
187 pub fn commit_envelope(
189 &mut self,
190 envelope: &RecordEnvelope,
191 signing_key: &SigningKey,
192 principal_did_hash: u64,
193 source: &str,
194 summary: Option<String>,
195 ) -> std::io::Result<usize> {
196 let mut buffer = [wellfare_core::record::NQuin::default(); 8];
197 let count = envelope.compile_to_quins(&mut buffer);
198 let mut committed = 0;
199 for i in 0..count {
200 let src = &buffer[i];
201 let q = NQuin {
202 subject: src.subject,
203 predicate: src.predicate,
204 object: src.object,
205 context: src.context,
206 metadata: src.metadata,
207 parity: src.parity,
208 };
209 self.commit_quin(q, signing_key, principal_did_hash)?;
210 committed += 1;
211 }
212
213 let committed_unix = SystemTime::now()
214 .duration_since(UNIX_EPOCH)
215 .map(|d| d.as_secs() as u32)
216 .unwrap_or(0);
217 let entry = JournalEntry::from_envelope(envelope, source, committed_unix, summary);
218 self.journal.append(&entry)?;
219 if envelope.sensitivity != SensitivityClass::Classified {
224 let outbox_entry = SyncOutboxEntry::from_envelope(envelope, committed_unix);
225 self.sync_outbox.enqueue(&outbox_entry)?;
226 }
227 Ok(committed)
228 }
229
230 pub fn append_receipt(&self, receipt: &ReceiptRecord) -> std::io::Result<()> {
231 self.receipts.append(receipt)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use ed25519_dalek::SigningKey;
239 use wellfare_core::record::{EpistemicStatus, EvidenceType, RecordEnvelope, SensitivityClass};
240
241 #[test]
242 fn checkpoint_persists_graph_and_meta() {
243 let dir = tempfile::tempdir().unwrap();
244 let wal_path = dir.path().join("test.wal");
245 let mut vault = VaultService::open(&wal_path, dir.path(), 0xBEEF).unwrap();
246 let signing_key = SigningKey::from_bytes(&[7u8; 32]);
247 let envelope = RecordEnvelope {
248 id: "urn:wellfair:weight:chk".into(),
249 owner_did: "did:wf:owner".into(),
250 author_did: "did:wf:owner".into(),
251 proxy_did: None,
252 epistemic_status: EpistemicStatus::Asserted,
253 evidence_type: EvidenceType::DeviceMeasured,
254 sensitivity: SensitivityClass::Restricted,
255 asserted_time_unix: 1_700_000_000,
256 valid_time_start_unix: None,
257 valid_time_end_unix: None,
258 predecessor_id: None,
259 blob_hash: Some("abc".into()),
260 tombstone: false,
261 };
262 vault
263 .commit_envelope(&envelope, &signing_key, 1, "test", None)
264 .unwrap();
265 let hash = vault.checkpoint().unwrap();
266 assert_ne!(hash, [0u8; 32]);
267 assert!(vault.graph_quin_count() > 0);
268 assert!(dir.path().join(checkpoint_store::META_FILE).exists());
269 assert!(dir.path().join(checkpoint_store::DAG_FILE).exists());
270
271 let wal_path2 = dir.path().join("test2.wal");
272 std::fs::copy(&wal_path, &wal_path2).unwrap();
273 let graph_before = vault.graph_quin_count();
274 let meta_before = vault.checkpoint_meta().unwrap();
275 let reopened = VaultService::open(&wal_path2, dir.path(), 0xBEEF).unwrap();
276 assert_eq!(reopened.graph_quin_count(), graph_before);
277 let meta_after = reopened.checkpoint_meta().unwrap();
278 assert_eq!(meta_after.graph_quin_count, meta_before.graph_quin_count);
279 assert_eq!(meta_after.last_hash_hex, meta_before.last_hash_hex);
280 #[cfg(not(target_arch = "wasm32"))]
281 assert!(dir.path().join(checkpoint_store::Q42_FILE).exists());
282 }
283
284 #[test]
285 fn classified_records_excluded_from_sync_outbox() {
286 let dir = tempfile::tempdir().unwrap();
287 let wal_path = dir.path().join("sanct.wal");
288 let mut vault = VaultService::open(&wal_path, dir.path(), 0xBEEF).unwrap();
289 let signing_key = SigningKey::from_bytes(&[3u8; 32]);
290
291 let restricted = RecordEnvelope {
292 id: "urn:wellfair:weight:r1".into(),
293 owner_did: "did:wf:owner".into(),
294 author_did: "did:wf:owner".into(),
295 proxy_did: None,
296 epistemic_status: EpistemicStatus::Asserted,
297 evidence_type: EvidenceType::DeviceMeasured,
298 sensitivity: SensitivityClass::Restricted,
299 asserted_time_unix: 1_700_000_000,
300 valid_time_start_unix: None,
301 valid_time_end_unix: None,
302 predecessor_id: None,
303 blob_hash: None,
304 tombstone: false,
305 };
306 let classified = RecordEnvelope {
307 id: "urn:wellfair:sanctuary_note:s1".into(),
308 owner_did: "did:wf:owner".into(),
309 author_did: "did:wf:owner".into(),
310 proxy_did: None,
311 epistemic_status: EpistemicStatus::Asserted,
312 evidence_type: EvidenceType::SelfReported,
313 sensitivity: SensitivityClass::Classified,
314 asserted_time_unix: 1_700_000_100,
315 valid_time_start_unix: None,
316 valid_time_end_unix: None,
317 predecessor_id: None,
318 blob_hash: None,
319 tombstone: false,
320 };
321
322 vault
323 .commit_envelope(&restricted, &signing_key, 1, "test", None)
324 .unwrap();
325 vault
326 .commit_envelope(&classified, &signing_key, 1, "test", None)
327 .unwrap();
328
329 assert_eq!(vault.journal_count().unwrap(), 2);
331 let outbox = vault.list_outbox(16).unwrap();
333 assert_eq!(outbox.len(), 1);
334 assert_eq!(outbox[0].record_id, "urn:wellfair:weight:r1");
335 assert!(outbox
336 .iter()
337 .all(|e| e.record_id != "urn:wellfair:sanctuary_note:s1"));
338 }
339}