Skip to main content

didbot_repo/
lib.rs

1//! An atproto repository: a signed commit over a Merkle search tree, and the
2//! CAR file that carries it.
3//!
4//! This is the layer that turns records a server happens to hold into
5//! something a stranger can check. Three steps, and each one is worthless
6//! without the next:
7//!
8//! 1. Every record is encoded canonically and named by the hash of those
9//!    bytes — `didbot-data`.
10//! 2. Those names are keyed into a Merkle search tree whose shape no
11//!    implementation gets to choose, giving one root CID over the whole
12//!    repository — `didbot_data::mst`.
13//! 3. An account's key signs a [`Commit`] naming that root, and the whole lot
14//!    goes out as a [`car`] file.
15//!
16//! A [`Proof`] is the fourth step and the one that makes the other three worth
17//! having: the blocks on one key's path through the tree, so a reader can
18//! check that one record is in the repository without being handed all of it.
19//! [`read_record`] is that reader, for a record another server holds.
20//!
21//! A repository is built whole by a [`Builder`], and after that changed one
22//! record at a time by a [`Draft`], which re-signs the edited tree as the next
23//! commit. Both land on the same repository for the same records.
24//!
25//! ```
26//! use didbot_key::SigningKey;
27//! use didbot_repo::Builder;
28//!
29//! let key = SigningKey::generate();
30//! let mut repo = Builder::new();
31//! repo.insert(
32//!     "com.example.thing",
33//!     "3lqmnopqrstuv",
34//!     &serde_json::json!({"text": "quernstone", "emoji": "🜁"}),
35//! )?;
36//! let repo = repo.commit("did:web:kestrel.example", "3lqmnopqrstuv", None, &key);
37//!
38//! // The signature is what a reader checks, with a key it got from the DID
39//! // document rather than from us.
40//! repo.commit().verify(&key.verifying_key())?;
41//! let car = repo.to_car();
42//! # Ok::<(), Box<dyn std::error::Error>>(())
43//! ```
44//!
45//! # Why this is not in `didbot-pds`
46//!
47//! The same reason `didbot-data` is not: this sits below the server
48//! rather than inside it. It has no clock, no store, no notion of an account
49//! and no opinion about when a commit should happen — a caller hands over
50//! records, a revision and a key, and gets bytes. That makes the whole of it
51//! testable without a server, and it keeps the largest crate in the workspace
52//! from growing the two things most likely to be reused elsewhere.
53//!
54//! # What is deliberately absent
55//!
56//! **A reader of its own exports.** [`car::read`] and [`read_record`] read
57//! what another server sends. Nothing here reads this crate's own output back
58//! to check it: the implementation that checks an export is not ours, and a
59//! decoder written to check our own encoder would agree with it about
60//! anything the two got wrong together.
61//!
62//! **A history.** `prev` is a parameter and this crate will chain commits if
63//! a caller supplies one, but nothing here keeps them.
64//!
65//! **Blobs.** No lexicon in this project carries one, so there is no block in
66//! a repository here that is not DAG-CBOR.
67
68#![forbid(unsafe_code)]
69
70pub mod car;
71mod commit;
72mod repository;
73mod verify;
74
75pub use commit::{Commit, CommitError, CommitKey, SignedCommit, REPO_VERSION};
76pub use didbot_data::mst::Rewritten;
77pub use repository::{tree_key, Builder, Draft, Proof, RepoError, Repository};
78pub use verify::{read_record, ProofError};