1use std::{collections::BTreeMap, fmt, path::Path, str::FromStr, sync::Arc};
4
5use kcode_commit_session::{CommitReceipt, CommitRequest};
6pub use kcode_kmap_mutations::{
7 Operation as KmapOperation, Outcome as KmapOutcome, OwnerSelection as KmapOwnerSelection,
8};
9use kcode_kweb_db::{
10 Error as KwebError, KwebDb, Node, NodeHistory, NodeId, ObjectId, Owner, Provenance,
11 TransactionId,
12};
13use kcode_kweb_idempotency::{ExecuteError, ReceiptLane, VersionedDigest};
14use kcode_server_object_envelopes::{StoredProvenance, decode_provenance, encode_provenance};
15use sha2::{Digest, Sha256};
16
17const MAX_EMBEDDED_PROVENANCE_BYTES: usize = 1024 * 1024;
18
19pub type Result<T> = std::result::Result<T, Error>;
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24#[non_exhaustive]
25pub enum ErrorKind {
26 InvalidInput,
28 NotFound,
30 Conflict,
32 Unavailable,
34 Internal,
36}
37
38#[derive(Debug)]
40pub struct Error {
41 kind: ErrorKind,
42 message: String,
43}
44
45impl Error {
46 pub fn kind(&self) -> ErrorKind {
48 self.kind
49 }
50
51 fn invalid(message: impl Into<String>) -> Self {
52 Self {
53 kind: ErrorKind::InvalidInput,
54 message: message.into(),
55 }
56 }
57
58 fn internal(error: impl fmt::Display) -> Self {
59 Self {
60 kind: ErrorKind::Internal,
61 message: error.to_string(),
62 }
63 }
64}
65
66impl fmt::Display for Error {
67 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68 formatter.write_str(&self.message)
69 }
70}
71
72impl std::error::Error for Error {}
73
74impl From<KwebError> for Error {
75 fn from(error: KwebError) -> Self {
76 let kind = match error {
77 KwebError::InvalidInput(_) | KwebError::InvalidTransaction(_) => {
78 ErrorKind::InvalidInput
79 }
80 KwebError::NotFound(_) => ErrorKind::NotFound,
81 KwebError::Busy(_) => ErrorKind::Conflict,
82 KwebError::Io(_)
83 | KwebError::Corrupt(_)
84 | KwebError::InvalidConfig(_)
85 | KwebError::OfflineUpgradeRequired(_) => ErrorKind::Internal,
86 };
87 Self {
88 kind,
89 message: error.to_string(),
90 }
91 }
92}
93
94impl From<kcode_commit_session::Error> for Error {
95 fn from(error: kcode_commit_session::Error) -> Self {
96 let kind = match error.kind() {
97 kcode_commit_session::ErrorKind::InvalidInput => ErrorKind::InvalidInput,
98 kcode_commit_session::ErrorKind::NotFound => ErrorKind::NotFound,
99 kcode_commit_session::ErrorKind::Conflict => ErrorKind::Conflict,
100 _ => ErrorKind::Internal,
101 };
102 Self {
103 kind,
104 message: error.to_string(),
105 }
106 }
107}
108
109impl From<kcode_kmap_mutations::Error> for Error {
110 fn from(error: kcode_kmap_mutations::Error) -> Self {
111 let kind = match error.kind() {
112 kcode_kmap_mutations::ErrorKind::InvalidInput => ErrorKind::InvalidInput,
113 kcode_kmap_mutations::ErrorKind::NotFound => ErrorKind::NotFound,
114 kcode_kmap_mutations::ErrorKind::StaleRevision => ErrorKind::Conflict,
115 kcode_kmap_mutations::ErrorKind::Unavailable => ErrorKind::Unavailable,
116 kcode_kmap_mutations::ErrorKind::Internal => ErrorKind::Internal,
117 _ => ErrorKind::Internal,
118 };
119 Self {
120 kind,
121 message: error.to_string(),
122 }
123 }
124}
125
126#[derive(Clone, Debug, Eq, PartialEq)]
128pub struct CreateProvenance {
129 pub idempotency_id: String,
131 pub value: StoredProvenance,
133 pub storage_provenance: Provenance,
135}
136
137#[derive(Clone, Debug, Eq, PartialEq)]
142pub struct NodeContents {
143 pub short_name: String,
144 pub short_description: String,
145 pub long_description: String,
146 pub owner: Owner,
147 pub fixed_connections: Vec<NodeId>,
148 pub recent_connections: Vec<NodeId>,
149}
150
151impl NodeContents {
152 fn into_data(self, objects: Vec<ObjectId>) -> kcode_kweb_db::NodeData {
153 kcode_kweb_db::NodeData {
154 short_name: self.short_name,
155 short_description: self.short_description,
156 long_description: self.long_description,
157 owner: self.owner,
158 fixed_connections: self.fixed_connections,
159 recent_connections: self.recent_connections,
160 objects,
161 }
162 }
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
167pub struct NodeWrite {
168 pub idempotency_id: String,
170 pub provenance_id: ObjectId,
172 pub author: String,
174 pub contents: NodeContents,
176}
177
178#[derive(Clone, Debug, Eq, PartialEq)]
180pub struct KmapWrite {
181 pub operation: KmapOperation,
182 pub expected_revisions: BTreeMap<NodeId, TransactionId>,
183 pub provenance: Provenance,
184}
185
186#[derive(Clone)]
188pub struct KwebManager {
189 database: Arc<KwebDb>,
190 receipts: ReceiptLane,
191}
192
193impl KwebManager {
194 pub fn open(database: KwebDb, receipt_database: impl AsRef<Path>) -> Result<Self> {
196 let receipts = ReceiptLane::open(receipt_database).map_err(receipt_lane_error)?;
197 Ok(Self {
198 database: Arc::new(database),
199 receipts,
200 })
201 }
202
203 pub fn get_node(&self, id: NodeId) -> Result<Node> {
205 self.database.get_node(id).map_err(Error::from)
206 }
207
208 pub fn get_node_history(&self, id: NodeId) -> Result<NodeHistory> {
210 self.database.get_node_history(id).map_err(Error::from)
211 }
212
213 pub fn get_object(&self, id: ObjectId) -> Result<Vec<u8>> {
215 self.database.get_object(id).map_err(Error::from)
216 }
217
218 pub fn get_object_with_provenance(&self, id: ObjectId) -> Result<(Vec<u8>, Provenance)> {
220 self.database
221 .get_object_with_provenance(id)
222 .map_err(Error::from)
223 }
224
225 pub fn create_provenance(&self, request: CreateProvenance) -> Result<ObjectId> {
227 let encoded =
228 encode_provenance(&request.value).map_err(|error| Error::invalid(error.to_string()))?;
229 let legacy_digest = legacy_provenance_request_digest(&request);
230 let digest = provenance_request_digest(&encoded, &request.storage_provenance);
231 let storage_provenance = request.storage_provenance.clone();
232 let result = self.with_idempotency(
233 &request.idempotency_id,
234 "create_provenance",
235 VersionedDigest::v2(digest, legacy_digest),
236 |result_id| {
237 let id = ObjectId::from_str(result_id).map_err(|error| {
238 Error::internal(format!("invalid stored provenance receipt: {error}"))
239 })?;
240 let (stored_bytes, creating_provenance) =
241 self.database.get_object_with_provenance(id)?;
242 let stored_value = decode_provenance(&stored_bytes).map_err(|error| {
243 Error::internal(format!("invalid stored provenance object {id}: {error}"))
244 })?;
245 Ok(stored_value == request.value
246 && creating_provenance == request.storage_provenance)
247 },
248 || Ok(None),
249 || {
250 let mut transaction = self.database.start_transaction(storage_provenance)?;
251 let id = transaction.create_object(encoded)?;
252 transaction.finalize()?;
253 Ok(id.to_string())
254 },
255 )?;
256 let id = ObjectId::from_str(&result).map_err(|error| {
257 Error::internal(format!("invalid stored provenance receipt: {error}"))
258 })?;
259 Ok(id)
260 }
261
262 pub fn create_node(&self, request: NodeWrite) -> Result<Node> {
264 let digest = node_request_digest("create_node", None, &request);
265 let result =
266 self.with_idempotency(
267 &request.idempotency_id,
268 "create_node",
269 VersionedDigest::v1(digest),
270 |_| Ok(true),
271 || Ok(None),
272 || {
273 let provenance = self.load_provenance(request.provenance_id)?;
274 let mut transaction = self.database.start_transaction(
275 transaction_provenance(&provenance, request.provenance_id, request.author),
276 )?;
277 let id = transaction.create_node(request.contents.into_data(Vec::new()))?;
278 transaction.finalize()?;
279 Ok(id.to_string())
280 },
281 )?;
282 let id = NodeId::from_str(&result)
283 .map_err(|error| Error::internal(format!("invalid stored node receipt: {error}")))?;
284 self.get_node(id)
285 }
286
287 pub fn update_node(&self, id: NodeId, request: NodeWrite) -> Result<Node> {
289 let digest = node_request_digest("update_node", Some(id), &request);
290 self.with_idempotency(
291 &request.idempotency_id,
292 "update_node",
293 VersionedDigest::v1(digest),
294 |_| Ok(true),
295 || Ok(None),
296 || {
297 let provenance = self.load_provenance(request.provenance_id)?;
298 let objects = self.database.get_node(id)?.data.objects;
299 let mut transaction = self.database.start_transaction(transaction_provenance(
300 &provenance,
301 request.provenance_id,
302 request.author,
303 ))?;
304 transaction.update_node(id, request.contents.into_data(objects))?;
305 transaction.finalize()?;
306 Ok(id.to_string())
307 },
308 )?;
309 self.get_node(id)
310 }
311
312 pub fn apply_kmap_operation(&self, request: KmapWrite) -> Result<KmapOutcome> {
314 let receipt_node = receipt_node(&request.operation)?;
315 if let Some(outcome) =
316 self.recover_kmap_operation(receipt_node, &request.operation, &request.provenance)?
317 {
318 return Ok(outcome);
319 }
320 kcode_kmap_mutations::apply(
321 &self.database,
322 kcode_kmap_mutations::Request {
323 operation: request.operation,
324 expected_revisions: request.expected_revisions,
325 provenance: request.provenance,
326 },
327 )
328 .map_err(Error::from)
329 }
330
331 pub fn store_object(&self, provenance: Provenance, bytes: Vec<u8>) -> Result<ObjectId> {
333 let mut transaction = self.database.start_transaction(provenance)?;
334 let id = transaction.create_object(bytes)?;
335 transaction.finalize()?;
336 Ok(id)
337 }
338
339 pub fn commit_session(&self, request: CommitRequest) -> Result<CommitReceipt> {
341 self.receipts
342 .with_exclusive(|path| {
343 kcode_commit_session::commit_session(&self.database, path, request)
344 })
345 .map_err(|error| map_execute_error(error, Error::from))
346 }
347
348 fn load_provenance(&self, id: ObjectId) -> Result<StoredProvenance> {
349 let bytes = self.database.get_object(id)?;
350 decode_provenance(&bytes).map_err(|error| {
351 Error::internal(format!("invalid stored provenance object {id}: {error}"))
352 })
353 }
354
355 fn recover_kmap_operation(
356 &self,
357 receipt_node: NodeId,
358 operation: &KmapOperation,
359 provenance: &Provenance,
360 ) -> Result<Option<KmapOutcome>> {
361 let history = match self.database.get_node_history(receipt_node) {
362 Ok(history) => history,
363 Err(KwebError::NotFound(_)) => return Ok(None),
364 Err(error) => return Err(error.into()),
365 };
366 let Some(entry) = history
367 .entries
368 .iter()
369 .find(|entry| entry.provenance == *provenance && (entry.created || entry.updated))
370 else {
371 return Ok(None);
372 };
373 let created_node_id = if matches!(operation, KmapOperation::CreateNode { .. }) {
374 entry
375 .data
376 .as_ref()
377 .and_then(|data| data.recent_connections.first())
378 .copied()
379 } else {
380 None
381 };
382 let mut affected_node_ids = mutated_nodes(operation);
383 if let Some(id) = created_node_id {
384 affected_node_ids.push(id);
385 }
386 Ok(Some(KmapOutcome {
387 transaction_id: entry.transaction_id,
388 created_node_id,
389 affected_node_ids,
390 }))
391 }
392
393 fn with_idempotency(
394 &self,
395 idempotency_id: &str,
396 operation: &'static str,
397 digest: VersionedDigest,
398 legacy_result_matches: impl FnOnce(&str) -> Result<bool>,
399 recover_unknown: impl FnOnce() -> Result<Option<String>>,
400 mutation: impl FnOnce() -> Result<String>,
401 ) -> Result<String> {
402 self.receipts
403 .execute(
404 idempotency_id,
405 operation,
406 digest,
407 legacy_result_matches,
408 recover_unknown,
409 mutation,
410 )
411 .map_err(|error| map_execute_error(error, |value| value))
412 }
413}
414
415fn receipt_lane_error(error: kcode_kweb_idempotency::Error) -> Error {
416 let kind = match error.kind() {
417 kcode_kweb_idempotency::ErrorKind::InvalidInput => ErrorKind::InvalidInput,
418 kcode_kweb_idempotency::ErrorKind::Conflict => ErrorKind::Conflict,
419 kcode_kweb_idempotency::ErrorKind::Internal => ErrorKind::Internal,
420 _ => ErrorKind::Internal,
421 };
422 Error {
423 kind,
424 message: error.to_string(),
425 }
426}
427
428fn map_execute_error<E>(error: ExecuteError<E>, operation: impl FnOnce(E) -> Error) -> Error {
429 match error {
430 ExecuteError::Lane(error) => receipt_lane_error(error),
431 ExecuteError::Operation(error) => operation(error),
432 }
433}
434
435fn receipt_node(operation: &KmapOperation) -> Result<NodeId> {
436 match operation {
437 KmapOperation::ConnectNodes(ids) => ids.first().copied(),
438 KmapOperation::ConsolidateFanout { parent, .. }
439 | KmapOperation::SetFixedConnection { parent, .. } => Some(*parent),
440 KmapOperation::CreateNode { parents, .. } => parents.first().copied(),
441 KmapOperation::UpdateNode { id, .. } => Some(*id),
442 }
443 .ok_or_else(|| Error::invalid("Kmap operation has no durable anchor node"))
444}
445
446fn mutated_nodes(operation: &KmapOperation) -> Vec<NodeId> {
447 let values = match operation {
448 KmapOperation::ConnectNodes(ids) => ids.clone(),
449 KmapOperation::ConsolidateFanout {
450 parent, aggregator, ..
451 } => {
452 vec![*parent, *aggregator]
453 }
454 KmapOperation::SetFixedConnection { parent, .. } => vec![*parent],
455 KmapOperation::CreateNode { parents, .. } => parents.clone(),
456 KmapOperation::UpdateNode { id, .. } => vec![*id],
457 };
458 values
459 .into_iter()
460 .collect::<std::collections::BTreeSet<_>>()
461 .into_iter()
462 .collect()
463}
464
465struct RequestDigest(Sha256);
466
467impl RequestDigest {
468 fn new(operation: &str) -> Self {
469 let mut hash = Sha256::new();
470 hash.update(b"kennedy kmap idempotency v1\0");
471 let mut value = Self(hash);
472 value.field(operation.as_bytes());
473 value
474 }
475
476 fn new_v2(operation: &str) -> Self {
477 let mut hash = Sha256::new();
478 hash.update(b"kennedy kweb manager idempotency v2\0");
479 let mut value = Self(hash);
480 value.field(operation.as_bytes());
481 value
482 }
483
484 fn field(&mut self, bytes: &[u8]) {
485 self.0.update((bytes.len() as u64).to_be_bytes());
486 self.0.update(bytes);
487 }
488
489 fn finish(self) -> [u8; 32] {
490 self.0.finalize().into()
491 }
492}
493
494fn provenance_request_digest(encoded: &[u8], storage: &Provenance) -> [u8; 32] {
495 let mut hash = RequestDigest::new_v2("create_provenance");
496 hash.field(encoded);
497 hash.field(storage.author.as_bytes());
498 hash.field(storage.source.as_bytes());
499 hash.field(&storage.source_created_at.timestamp().to_be_bytes());
500 hash.field(
501 &storage
502 .source_created_at
503 .timestamp_subsec_nanos()
504 .to_be_bytes(),
505 );
506 hash.field(storage.data.as_bytes());
507 hash.finish()
508}
509
510fn legacy_provenance_request_digest(request: &CreateProvenance) -> [u8; 32] {
514 let mut hash = RequestDigest::new("create_provenance");
515 hash.field(request.value.data.as_bytes());
516 hash.field(request.value.source.as_bytes());
517 hash.field(&request.value.source_created_at.timestamp().to_be_bytes());
518 hash.field(
519 &request
520 .value
521 .source_created_at
522 .timestamp_subsec_nanos()
523 .to_be_bytes(),
524 );
525 hash.field(b"");
526 hash.field(&(request.value.artifacts.len() as u64).to_be_bytes());
527 for artifact in &request.value.artifacts {
528 hash.field(artifact.original_filename.as_bytes());
529 hash.field(artifact.media_type.as_bytes());
530 hash.field(&artifact.sha256);
531 }
532 hash.field(request.storage_provenance.author.as_bytes());
533 hash.field(request.storage_provenance.source.as_bytes());
534 hash.field(
535 &request
536 .storage_provenance
537 .source_created_at
538 .timestamp()
539 .to_be_bytes(),
540 );
541 hash.field(
542 &request
543 .storage_provenance
544 .source_created_at
545 .timestamp_subsec_nanos()
546 .to_be_bytes(),
547 );
548 hash.field(request.storage_provenance.data.as_bytes());
549 hash.finish()
550}
551
552fn node_request_digest(operation: &str, id: Option<NodeId>, request: &NodeWrite) -> [u8; 32] {
553 let mut hash = RequestDigest::new(operation);
554 hash.field(&id.map(NodeId::to_bytes).unwrap_or([0; 6]));
555 hash.field(&request.provenance_id.to_bytes());
556 hash.field(request.author.as_bytes());
557 hash.field(request.contents.short_name.as_bytes());
558 hash.field(request.contents.short_description.as_bytes());
559 hash.field(request.contents.long_description.as_bytes());
560 match request.contents.owner {
561 Owner::Unowned => hash.field(&[0]),
562 Owner::SelfNode => hash.field(&[1]),
563 Owner::Node(owner) => {
564 hash.field(&[2]);
565 hash.field(&owner.to_bytes());
566 }
567 }
568 hash.field(&(request.contents.fixed_connections.len() as u64).to_be_bytes());
569 for connection in &request.contents.fixed_connections {
570 hash.field(&connection.to_bytes());
571 }
572 hash.field(&(request.contents.recent_connections.len() as u64).to_be_bytes());
573 for connection in &request.contents.recent_connections {
574 hash.field(&connection.to_bytes());
575 }
576 hash.finish()
577}
578
579fn transaction_provenance(
580 stored: &StoredProvenance,
581 object_id: ObjectId,
582 author: String,
583) -> Provenance {
584 let data = if stored.data.len() <= MAX_EMBEDDED_PROVENANCE_BYTES {
585 stored.data.clone()
586 } else {
587 format!("Kennedy provenance is stored in object {object_id}.")
588 };
589 Provenance {
590 author,
591 source: stored.source.clone(),
592 source_created_at: stored.source_created_at,
593 data,
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use std::{
600 collections::BTreeMap,
601 fs,
602 path::PathBuf,
603 sync::{
604 Arc,
605 atomic::{AtomicU64, Ordering},
606 },
607 };
608
609 use chrono::{Duration, TimeZone, Utc};
610 use kcode_commit_session::CommitRequest;
611 use kcode_kweb_db::{Config, NodeData, NoopGossip, WriterId};
612 use kcode_server_object_envelopes::{StoredArtifact, decode_provenance};
613 use rusqlite::{Connection, params};
614
615 use super::*;
616
617 static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(1);
618
619 struct TestDirectory(PathBuf);
620
621 impl TestDirectory {
622 fn new() -> Self {
623 let sequence = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed);
624 let path = std::env::temp_dir().join(format!(
625 "kcode-kweb-manager-test-{}-{sequence}",
626 std::process::id()
627 ));
628 fs::create_dir_all(&path).unwrap();
629 Self(path)
630 }
631
632 fn join(&self, value: &str) -> PathBuf {
633 self.0.join(value)
634 }
635 }
636
637 impl Drop for TestDirectory {
638 fn drop(&mut self) {
639 fs::remove_dir_all(&self.0).unwrap();
640 }
641 }
642
643 fn config() -> Config {
644 let signing_key = [7; 32];
645 Config {
646 signing_key,
647 writers_by_priority: vec![WriterId::from_signing_key(&signing_key)],
648 gossip: Arc::new(NoopGossip),
649 }
650 }
651
652 fn timestamp() -> chrono::DateTime<Utc> {
653 Utc.with_ymd_and_hms(2026, 7, 28, 12, 0, 0).unwrap()
654 }
655
656 fn transaction_provenance_for(label: &str) -> Provenance {
657 Provenance {
658 author: "test".into(),
659 source: label.into(),
660 source_created_at: timestamp(),
661 data: format!("{label} transaction"),
662 }
663 }
664
665 fn provenance_request(idempotency_id: &str, data: &str) -> CreateProvenance {
666 CreateProvenance {
667 idempotency_id: idempotency_id.into(),
668 value: StoredProvenance {
669 data: data.into(),
670 source: "test-source".into(),
671 source_created_at: timestamp(),
672 artifacts: Vec::new(),
673 },
674 storage_provenance: transaction_provenance_for("provenance-storage"),
675 }
676 }
677
678 fn artifact() -> StoredArtifact {
679 StoredArtifact {
680 object_id: ObjectId::from_bytes([0x80, 1, 2, 3, 4, 5]).unwrap(),
681 original_filename: "source.txt".into(),
682 media_type: "text/plain".into(),
683 role: "source".into(),
684 byte_length: 12,
685 sha256: [7; 32],
686 }
687 }
688
689 fn node_write(idempotency_id: &str, provenance_id: ObjectId, short_name: &str) -> NodeWrite {
690 NodeWrite {
691 idempotency_id: idempotency_id.into(),
692 provenance_id,
693 author: "test-model".into(),
694 contents: NodeContents {
695 short_name: short_name.into(),
696 short_description: "short".into(),
697 long_description: "long".into(),
698 owner: Owner::SelfNode,
699 fixed_connections: Vec::new(),
700 recent_connections: Vec::new(),
701 },
702 }
703 }
704
705 #[test]
706 fn provenance_and_node_mutations_are_idempotent_and_typed() {
707 let directory = TestDirectory::new();
708 let database = KwebDb::open(directory.join("kweb"), config()).unwrap();
709 let kmap = KwebManager::open(database, directory.join("application.sqlite3")).unwrap();
710
711 let create_request =
712 provenance_request("00000000000000000000000000000001", "source material");
713 let provenance_id = kmap.create_provenance(create_request.clone()).unwrap();
714 assert_eq!(
715 kmap.create_provenance(create_request).unwrap(),
716 provenance_id
717 );
718 let stored = decode_provenance(&kmap.get_object(provenance_id).unwrap()).unwrap();
719 assert_eq!(stored.data, "source material");
720
721 let conflict = kmap
722 .create_provenance(provenance_request(
723 "00000000000000000000000000000001",
724 "different material",
725 ))
726 .unwrap_err();
727 assert_eq!(conflict.kind(), ErrorKind::Conflict);
728
729 let write = node_write(
730 "00000000000000000000000000000002",
731 provenance_id,
732 "Created node",
733 );
734 let node = kmap.create_node(write.clone()).unwrap();
735 assert_eq!(kmap.create_node(write).unwrap(), node);
736 assert_eq!(kmap.get_node(node.id).unwrap(), node);
737 assert!(node.data.objects.is_empty());
738 }
739
740 #[test]
741 fn provenance_idempotency_includes_every_storage_provenance_field() {
742 let directory = TestDirectory::new();
743 let database = KwebDb::open(directory.join("kweb"), config()).unwrap();
744 let kmap = KwebManager::open(database, directory.join("application.sqlite3")).unwrap();
745
746 let request = provenance_request("00000000000000000000000000000005", "source material");
747 let provenance_id = kmap.create_provenance(request.clone()).unwrap();
748 assert_eq!(
749 kmap.create_provenance(request.clone()).unwrap(),
750 provenance_id
751 );
752
753 let mut changed_author = request.clone();
754 changed_author
755 .storage_provenance
756 .author
757 .push_str("-changed");
758 let mut changed_source = request.clone();
759 changed_source
760 .storage_provenance
761 .source
762 .push_str("-changed");
763 let mut changed_timestamp = request.clone();
764 changed_timestamp.storage_provenance.source_created_at += Duration::nanoseconds(1);
765 let mut changed_data = request.clone();
766 changed_data.storage_provenance.data.push_str("-changed");
767
768 for changed in [
769 changed_author,
770 changed_source,
771 changed_timestamp,
772 changed_data,
773 ] {
774 let conflict = kmap.create_provenance(changed).unwrap_err();
775 assert_eq!(conflict.kind(), ErrorKind::Conflict);
776 }
777
778 let (bytes, creating_provenance) = kmap.get_object_with_provenance(provenance_id).unwrap();
779 assert_eq!(decode_provenance(&bytes).unwrap(), request.value);
780 assert_eq!(creating_provenance, request.storage_provenance);
781 }
782
783 #[test]
784 fn provenance_idempotency_includes_every_artifact_field() {
785 let directory = TestDirectory::new();
786 let database = KwebDb::open(directory.join("kweb"), config()).unwrap();
787 let kmap = KwebManager::open(database, directory.join("application.sqlite3")).unwrap();
788 let mut request = provenance_request("00000000000000000000000000000006", "source material");
789 request.value.artifacts.push(artifact());
790 let provenance_id = kmap.create_provenance(request.clone()).unwrap();
791 assert_eq!(
792 kmap.create_provenance(request.clone()).unwrap(),
793 provenance_id
794 );
795
796 let mut changed_object = request.clone();
797 changed_object.value.artifacts[0].object_id =
798 ObjectId::from_bytes([0x80, 1, 2, 3, 4, 6]).unwrap();
799 let mut changed_filename = request.clone();
800 changed_filename.value.artifacts[0].original_filename = "other.txt".into();
801 let mut changed_media_type = request.clone();
802 changed_media_type.value.artifacts[0].media_type = "application/json".into();
803 let mut changed_role = request.clone();
804 changed_role.value.artifacts[0].role = "transcript".into();
805 let mut changed_size = request.clone();
806 changed_size.value.artifacts[0].byte_length += 1;
807 let mut changed_sha256 = request;
808 changed_sha256.value.artifacts[0].sha256[0] ^= 1;
809
810 for changed in [
811 changed_object,
812 changed_filename,
813 changed_media_type,
814 changed_role,
815 changed_size,
816 changed_sha256,
817 ] {
818 let conflict = kmap.create_provenance(changed).unwrap_err();
819 assert_eq!(conflict.kind(), ErrorKind::Conflict);
820 }
821 }
822
823 #[test]
824 fn legacy_provenance_receipts_replay_exactly_and_drop_duplicate_storage() {
825 let directory = TestDirectory::new();
826 let receipt_path = directory.join("application.sqlite3");
827 let database = KwebDb::open(directory.join("kweb"), config()).unwrap();
828 let mut request = provenance_request("00000000000000000000000000000007", "legacy source");
829 request.value.artifacts.push(artifact());
830 let encoded = encode_provenance(&request.value).unwrap();
831 let mut transaction = database
832 .start_transaction(request.storage_provenance.clone())
833 .unwrap();
834 let object_id = transaction.create_object(encoded).unwrap();
835 transaction.finalize().unwrap();
836
837 let receipts = Connection::open(&receipt_path).unwrap();
838 receipts
839 .execute_batch(
840 "CREATE TABLE kmap_idempotency_receipts (
841 idempotency_id TEXT PRIMARY KEY CHECK(length(idempotency_id)=32),
842 operation TEXT NOT NULL,
843 request_sha256 BLOB NOT NULL CHECK(length(request_sha256)=32),
844 result_id TEXT CHECK(result_id IS NULL OR length(result_id)=8),
845 started_at TEXT NOT NULL,
846 committed_at TEXT,
847 CHECK((result_id IS NULL) = (committed_at IS NULL))
848 );
849 CREATE TABLE kmap_object_provenance(object_id TEXT PRIMARY KEY);",
850 )
851 .unwrap();
852 receipts
853 .execute(
854 "INSERT INTO kmap_idempotency_receipts(
855 idempotency_id,operation,request_sha256,result_id,started_at,committed_at
856 ) VALUES(?1,'create_provenance',?2,?3,?4,?4)",
857 params![
858 &request.idempotency_id,
859 legacy_provenance_request_digest(&request).as_slice(),
860 object_id.to_string(),
861 Utc::now().to_rfc3339(),
862 ],
863 )
864 .unwrap();
865 drop(receipts);
866
867 let kmap = KwebManager::open(database, &receipt_path).unwrap();
868 assert_eq!(kmap.create_provenance(request.clone()).unwrap(), object_id);
869 let mut changed_role = request;
870 changed_role.value.artifacts[0].role = "different".into();
871 let conflict = kmap.create_provenance(changed_role).unwrap_err();
872 assert_eq!(conflict.kind(), ErrorKind::Conflict);
873
874 let receipts = Connection::open(receipt_path).unwrap();
875 let duplicate_table_count = receipts
876 .query_row(
877 "SELECT COUNT(*) FROM sqlite_master
878 WHERE type='table' AND name='kmap_object_provenance'",
879 [],
880 |row| row.get::<_, i64>(0),
881 )
882 .unwrap();
883 assert_eq!(duplicate_table_count, 0);
884 let digest_version = receipts
885 .query_row(
886 "SELECT digest_version FROM kmap_idempotency_receipts
887 WHERE idempotency_id=?1",
888 [&"00000000000000000000000000000007"],
889 |row| row.get::<_, i64>(0),
890 )
891 .unwrap();
892 assert_eq!(digest_version, 1);
893 }
894
895 #[test]
896 fn ordinary_updates_preserve_existing_object_attachments() {
897 let directory = TestDirectory::new();
898 let database = KwebDb::open(directory.join("kweb"), config()).unwrap();
899 let mut transaction = database
900 .start_transaction(transaction_provenance_for("seed"))
901 .unwrap();
902 let object_id = transaction.create_object(b"attachment".to_vec()).unwrap();
903 let node_id = transaction
904 .create_node(NodeData {
905 short_name: "Before".into(),
906 short_description: String::new(),
907 long_description: String::new(),
908 owner: Owner::SelfNode,
909 fixed_connections: Vec::new(),
910 recent_connections: Vec::new(),
911 objects: vec![object_id],
912 })
913 .unwrap();
914 transaction.finalize().unwrap();
915
916 let kmap = KwebManager::open(database, directory.join("application.sqlite3")).unwrap();
917 let (stored_bytes, stored_provenance) = kmap.get_object_with_provenance(object_id).unwrap();
918 assert_eq!(stored_bytes, b"attachment");
919 assert_eq!(stored_provenance, transaction_provenance_for("seed"));
920
921 let provenance_id = kmap
922 .create_provenance(provenance_request(
923 "00000000000000000000000000000003",
924 "update source",
925 ))
926 .unwrap();
927 let updated = kmap
928 .update_node(
929 node_id,
930 node_write("00000000000000000000000000000004", provenance_id, "After"),
931 )
932 .unwrap();
933 assert_eq!(updated.data.short_name, "After");
934 assert_eq!(updated.data.objects, vec![object_id]);
935 }
936
937 #[test]
938 fn object_storage_and_session_commits_share_the_owned_database() {
939 let directory = TestDirectory::new();
940 let database = KwebDb::open(directory.join("kweb"), config()).unwrap();
941 let kmap = KwebManager::open(database, directory.join("application.sqlite3")).unwrap();
942
943 let opaque_provenance = transaction_provenance_for("opaque-object");
944 let object_id = kmap
945 .store_object(opaque_provenance.clone(), b"opaque bytes".to_vec())
946 .unwrap();
947 assert_eq!(kmap.get_object(object_id).unwrap(), b"opaque bytes");
948 let (opaque_bytes, retained_provenance) =
949 kmap.get_object_with_provenance(object_id).unwrap();
950 assert_eq!(opaque_bytes, b"opaque bytes");
951 assert_eq!(retained_provenance, opaque_provenance);
952
953 let request = CommitRequest {
954 idempotency_key: "session-test".into(),
955 author: "test-model".into(),
956 source_created_at: timestamp(),
957 archive: b"{\"events\":[]}".to_vec(),
958 objects: BTreeMap::new(),
959 creates: BTreeMap::new(),
960 updates: BTreeMap::new(),
961 };
962 let first = kmap.commit_session(request.clone()).unwrap();
963 assert_eq!(
964 kmap.get_object(first.session_object_id).unwrap(),
965 b"{\"events\":[]}"
966 );
967 let (archive, provenance) = kmap
968 .get_object_with_provenance(first.session_object_id)
969 .unwrap();
970 assert_eq!(archive, b"{\"events\":[]}");
971 assert_eq!(provenance.author, "test-model");
972 assert_eq!(provenance.source, "kennedy-session");
973 assert_eq!(kmap.commit_session(request).unwrap(), first);
974 }
975}