eidetica/database/mod.rs
1//! Database module provides functionality for managing collections of related entries.
2//!
3//! A `Database` represents a hierarchical structure of entries, like a traditional database
4//! or a branch in a version control system. Each database has a root entry and maintains
5//! the history and relationships between entries. Database holds a weak reference to its
6//! parent Instance, accessing storage and coordination services through that handle.
7
8use std::{future::Future, sync::Arc};
9
10use rand::{Rng, RngCore, distributions::Alphanumeric};
11use serde_json;
12
13#[cfg(all(unix, feature = "service"))]
14use crate::instance::backend::RemoteBackend;
15#[cfg(all(unix, feature = "service"))]
16use crate::service::client::RemoteConnection;
17use crate::{
18 Error, Instance, Result, Snapshot, Transaction, WeakInstance,
19 auth::{
20 crypto::{PrivateKey, PublicKey},
21 errors::AuthError,
22 settings::AuthSettings,
23 types::{AuthKey, Permission, SigKey},
24 validation::AuthValidator,
25 },
26 backend::VerificationStatus,
27 constants::{ROOT, SETTINGS},
28 crdt::{CRDT, Doc},
29 entry::{Entry, ID},
30 instance::{WriteCallback, WriteEvent, backend::Backend, errors::InstanceError},
31 store::{SettingsStore, Store},
32};
33
34#[cfg(test)]
35mod tests;
36
37tokio::task_local! {
38 /// Set while a `verify()`/validation pass is on the call stack.
39 ///
40 /// Verification reads the database (delegation resolution opens trees,
41 /// reads settings → tips), and the access-time auto-verify hook in
42 /// [`Database::snapshot`] would otherwise re-enter verification
43 /// unboundedly. While this is set, the hook is suppressed and reads
44 /// return raw (still `Failed`-filtered) tips.
45 static IN_VERIFY: bool;
46}
47
48fn auto_verify_suppressed() -> bool {
49 IN_VERIFY.try_with(|v| *v).unwrap_or(false)
50}
51
52/// Outcome of reconstructing the `_settings` state an entry pins.
53///
54/// An entry records, in its signed metadata, the `_settings` tips its
55/// signature must be validated against. We can only verify it if this node
56/// holds that full pinned `_settings` ancestor set.
57enum PinnedSettings {
58 /// The pinned `_settings` set is fully present; here is its auth config.
59 Complete(AuthSettings),
60 /// This node does not yet hold the full pinned `_settings` set, so the
61 /// entry cannot be verified yet (it stays `Unverified`).
62 Incomplete,
63}
64
65/// Summary of a [`Database::verify`] pass.
66#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
67pub struct VerifyReport {
68 /// Entries promoted `Unverified` → `Verified` this pass.
69 pub verified: usize,
70 /// Entries marked `Unverified` → `Failed` (definitively bad) this pass.
71 pub failed: usize,
72 /// Entries left `Unverified` (pinned `_settings` not yet held locally).
73 pub still_unverified: usize,
74}
75
76/// A signing key bound to its identity in a database's auth settings.
77///
78/// Pairs the cryptographic signing key with information about how to look up
79/// permissions in the database's auth configuration. The identity determines
80/// which entry in `_settings.auth` this key maps to.
81#[derive(Clone, Debug)]
82pub struct DatabaseKey {
83 signing_key: Box<PrivateKey>,
84 identity: SigKey,
85}
86
87impl DatabaseKey {
88 /// Identity = pubkey derived from signing key. Most common case.
89 pub fn new(signing_key: PrivateKey) -> Self {
90 let pubkey = signing_key.public_key();
91 Self {
92 signing_key: Box::new(signing_key),
93 identity: SigKey::from_pubkey(&pubkey),
94 }
95 }
96
97 /// Identity = explicit SigKey (name, global, delegation, etc.)
98 pub fn with_identity(signing_key: PrivateKey, identity: SigKey) -> Self {
99 Self {
100 signing_key: Box::new(signing_key),
101 identity,
102 }
103 }
104
105 /// Identity = global permission with actual pubkey embedded for verification.
106 pub fn global(signing_key: PrivateKey) -> Self {
107 let pubkey = signing_key.public_key();
108 Self {
109 signing_key: Box::new(signing_key),
110 identity: SigKey::global(&pubkey),
111 }
112 }
113
114 /// Identity = key name lookup.
115 pub fn with_name(signing_key: PrivateKey, name: impl Into<String>) -> Self {
116 Self {
117 signing_key: Box::new(signing_key),
118 identity: SigKey::from_name(name),
119 }
120 }
121
122 /// Get the signing key.
123 pub fn signing_key(&self) -> &PrivateKey {
124 &self.signing_key
125 }
126
127 /// Get the public key.
128 pub fn public_key(&self) -> PublicKey {
129 self.signing_key.public_key()
130 }
131
132 /// Get the identity used for auth settings lookup.
133 pub fn identity(&self) -> &SigKey {
134 &self.identity
135 }
136
137 /// Consume self and return the parts.
138 pub fn into_parts(self) -> (PrivateKey, SigKey) {
139 (*self.signing_key, self.identity)
140 }
141}
142
143impl From<PrivateKey> for DatabaseKey {
144 /// Convert a `PrivateKey` into a `DatabaseKey` with pubkey-derived identity.
145 ///
146 /// This is equivalent to [`DatabaseKey::new`] and covers the most common case
147 /// where the key's identity in auth settings is its own public key.
148 fn from(signing_key: PrivateKey) -> Self {
149 Self::new(signing_key)
150 }
151}
152
153/// Represents a collection of related entries, like a traditional database or a branch in a version control system.
154///
155/// Each `Database` is identified by the ID of its root `Entry` and manages the history of data
156/// associated with that root. It interacts with the underlying storage through the Instance handle.
157#[derive(Clone, Debug)]
158pub struct Database {
159 root: ID,
160 instance: WeakInstance,
161 /// Storage seam `Transaction`/`Store` reads flow through. On a local
162 /// instance this is a clone of the instance's own [`Backend`] (forwarding
163 /// to the backing engine); on a connected instance a per-handle
164 /// [`RemoteBackend`] bound to this database's acting identity. Derived from
165 /// `instance`/construction only — carrying it across
166 /// `with_key`/`allow_unverified` (`..self`) rebuilds is correct.
167 ops: Arc<dyn Backend>,
168 /// Signing key bound to its auth identity for this database
169 key: Option<DatabaseKey>,
170 /// When `false` (default), reads expose only the maximal all-`Verified`
171 /// prefix of the DAG (the "Verified frontier"). When `true`, reads also
172 /// include `Unverified` entries. `Failed` entries are dropped regardless.
173 /// Set via [`Database::allow_unverified`].
174 allow_unverified: bool,
175}
176
177impl Database {
178 /// Creates a new `Database` instance with a user-provided signing key.
179 ///
180 /// This constructor creates a new database using a signing key that's already in memory
181 /// (e.g., from UserKeyManager), without requiring the key to be stored in the backend.
182 /// This is the preferred method for creating databases in a User context where keys
183 /// are managed separately from the backend.
184 ///
185 /// The created database will use a `DatabaseKey` for all subsequent operations,
186 /// meaning transactions will use the provided key directly rather than looking it up
187 /// from backend storage.
188 ///
189 /// # Auth Bootstrapping
190 ///
191 /// Auth is always bootstrapped with the signing key as `Admin(0)`. Passing auth
192 /// configuration in `initial_settings` is an error — additional keys must be added
193 /// via follow-up transactions after creation.
194 ///
195 /// # Arguments
196 /// * `instance` - Instance handle for storage and coordination
197 /// * `signing_key` - The signing key to use for the initial commit and subsequent operations.
198 /// This key should already be decrypted and ready to use. The public key is derived
199 /// automatically and used as the key identifier in auth settings.
200 /// * `initial_settings` - `Doc` CRDT containing the initial settings for the database.
201 /// Use `Doc::new()` for an empty settings document.
202 ///
203 /// # Returns
204 /// A `Result` containing the new `Database` instance configured with a `DatabaseKey`.
205 ///
206 /// # Example
207 /// ```rust,no_run
208 /// # use eidetica::*;
209 /// # use eidetica::backend::database::InMemory;
210 /// # use eidetica::auth::crypto::generate_keypair;
211 /// # use eidetica::crdt::Doc;
212 /// # #[tokio::main]
213 /// # async fn main() -> Result<()> {
214 /// let instance = Instance::open_backend(Box::new(InMemory::new())).await?;
215 /// let (signing_key, _public_key) = generate_keypair();
216 ///
217 /// let mut settings = Doc::new();
218 /// settings.set("name", "my_database");
219 ///
220 /// // Create database with user-managed key (no backend storage needed)
221 /// let database = Database::create(&instance, signing_key, settings).await?;
222 ///
223 /// // All transactions automatically use the provided key
224 /// let tx = database.new_transaction().await?;
225 /// # Ok(())
226 /// # }
227 /// ```
228 pub async fn create(
229 instance: &Instance,
230 signing_key: PrivateKey,
231 initial_settings: Doc,
232 ) -> Result<Self> {
233 Self::create_with_init(instance, signing_key, initial_settings, async |_| Ok(())).await
234 }
235
236 /// Creates a new database with an initialization callback that runs inside
237 /// the genesis transaction.
238 ///
239 /// This is the underlying constructor used by [`Self::create`] and by
240 /// `User::new_database()` (the builder API). The callback receives the
241 /// genesis transaction after `_settings` and `_root` have been staged but
242 /// before commit, allowing additional subtrees — stores, initial doc data,
243 /// records — to be written into the same entry that establishes the
244 /// database root.
245 ///
246 /// All writes performed in the callback become part of the single genesis
247 /// entry: one signed entry, one backend write, atomic. If the callback
248 /// returns an error, the transaction is dropped without committing and no
249 /// database is created.
250 ///
251 /// # Arguments
252 /// * `instance` - Instance handle for storage and coordination
253 /// * `signing_key` - The private key for this database (becomes Admin(0))
254 /// * `initial_settings` - Initial settings document (must not contain auth)
255 /// * `init` - Callback run against the genesis transaction before commit
256 pub async fn create_with_init<F>(
257 instance: &Instance,
258 signing_key: PrivateKey,
259 initial_settings: Doc,
260 init: F,
261 ) -> Result<Self>
262 where
263 F: AsyncFnOnce(&Transaction) -> Result<()>,
264 {
265 let mut initial_settings = initial_settings;
266 let pubkey = signing_key.public_key();
267
268 // Reject preconfigured auth — Database::create owns auth bootstrapping entirely.
269 if initial_settings.get("auth").is_some() {
270 return Err(Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
271 reason: "initial_settings must not contain auth configuration; \
272 Database::create bootstraps auth with the signing key as Admin(0)"
273 .to_string(),
274 })));
275 }
276
277 // Bootstrap auth with the signing key as Admin(0)
278 let mut auth_settings = AuthSettings::new();
279 auth_settings.add_key(&pubkey, AuthKey::active(None, Permission::Admin(0)))?;
280 initial_settings.set("auth", auth_settings.as_doc().clone());
281
282 // Create the initial root entry using a temporary Database and Transaction.
283 // This placeholder ID should not exist in the backend, so snapshot will be empty.
284 let bootstrap_placeholder_id = format!(
285 "bootstrap_root_{}",
286 rand::thread_rng()
287 .sample_iter(&Alphanumeric)
288 .take(10)
289 .map(char::from)
290 .collect::<String>()
291 );
292
293 // Create temporary database for bootstrap with DatabaseKey.
294 // This allows the bootstrap transaction to use the provided key directly.
295 let temp_database_for_bootstrap = Database {
296 root: ID::from_bytes(bootstrap_placeholder_id.as_bytes()),
297 instance: instance.downgrade(),
298 ops: instance.backend().clone(),
299 key: Some(DatabaseKey::new(signing_key.clone())),
300 allow_unverified: false,
301 };
302
303 // Create the transaction - it will use the provided key automatically
304 let txn = temp_database_for_bootstrap.new_transaction().await?;
305
306 // IMPORTANT: For the root entry, we need to set the database root to empty/default
307 // so that is_root() returns true and all_roots() can find it
308 txn.set_entry_root(ID::default())?;
309
310 // Populate the SETTINGS and ROOT subtrees for the very first entry
311 txn.update_subtree(SETTINGS, serde_json::to_vec(&initial_settings)?)
312 .await?;
313 txn.update_subtree(ROOT, serde_json::to_vec("")?).await?; // Standard practice for root entry's _root
314
315 // Add entropy to the entry metadata to ensure unique database IDs even with identical settings
316 txn.set_metadata_entropy(rand::thread_rng().next_u64())?;
317
318 // Lock system subtrees (`_settings`, `_root`, `_index`) for the
319 // duration of the init callback. The callback gets a `&Transaction`
320 // that rejects `_*` opens via `get_store` / `Store::open`, so callers
321 // can't accidentally clobber subtrees that `create_with_init` itself
322 // manages. Internal paths (Registry, Store::register's `_index`
323 // writes) bypass `get_store` and remain functional. The guard releases
324 // on drop, so the lock lifts on both early-return and panic-unwind.
325 {
326 let _lock = txn.lock_system_subtrees();
327 init(&txn).await?;
328 }
329
330 // Commit the initial entry
331 let new_root_id = txn.commit().await?;
332
333 // Construct the returned Database, wiring its `ops` to match the
334 // instance's flavour:
335 //
336 // - **Connected (remote) instance** — bind a `RemoteBackend` to the
337 // *new database's* identity (the signing-key's pubkey, self-signed
338 // as `Admin(0)` by the genesis). The connection's login pubkey is
339 // the *caller's* (e.g. the registering admin), which is **not** a
340 // member of the new tree's auth. If we cloned the instance's
341 // session backend here, every read `Transaction::commit` performs
342 // on this database would carry the connection's login identity, and
343 // the server's per-tree gate would deny it. A per-database identity
344 // makes all reads use the tree's own member key — but the server's
345 // gate also requires that key to be in the connection's *session
346 // keyset*, so we `register_session_key(signing_key)` first to do the
347 // proof-of-possession handshake that adds it.
348 // - **Local instance** — clone the instance's backend, unchanged.
349 #[cfg(all(unix, feature = "service"))]
350 if let Some(conn) = instance.remote_connection() {
351 let pubkey_for_identity = signing_key.public_key();
352 conn.register_session_key(&signing_key).await?;
353 return Ok(Self {
354 root: new_root_id.clone(),
355 instance: instance.downgrade(),
356 ops: Arc::new(RemoteBackend::new(
357 conn,
358 Some(SigKey::from_pubkey(&pubkey_for_identity)),
359 )),
360 key: Some(DatabaseKey::new(signing_key)),
361 allow_unverified: false,
362 });
363 }
364
365 Ok(Self {
366 root: new_root_id,
367 instance: instance.downgrade(),
368 ops: instance.backend().clone(),
369 key: Some(DatabaseKey::new(signing_key)),
370 allow_unverified: false,
371 })
372 }
373
374 /// Opens an existing database by its root ID.
375 ///
376 /// Verifies the root entry exists in the backend, then returns a handle
377 /// for read-only access. To perform authenticated writes, chain
378 /// `.with_key(key)` after opening.
379 ///
380 /// # Arguments
381 /// * `instance` - Instance handle for storage and coordination
382 /// * `root_id` - The root entry ID of the database to open
383 ///
384 /// # Errors
385 /// Returns an error if the root entry does not exist in the backend.
386 ///
387 /// # Example
388 /// ```rust,no_run
389 /// # use eidetica::*;
390 /// # use eidetica::backend::database::InMemory;
391 /// # use eidetica::auth::crypto::generate_keypair;
392 /// # #[tokio::main]
393 /// # async fn main() -> Result<()> {
394 /// # let instance = Instance::open_backend(Box::new(InMemory::new())).await?;
395 /// # let (signing_key, _verifying_key) = generate_keypair();
396 /// # let root_id = ID::from_bytes(b"existing_database_root_id");
397 /// // Open database for reading
398 /// let db = Database::open(&instance, &root_id).await?;
399 ///
400 /// // Open database with a signing key for writes
401 /// let db = Database::open(&instance, &root_id).await?.with_key(signing_key);
402 /// let tx = db.new_transaction().await?;
403 /// # Ok(())
404 /// # }
405 /// ```
406 pub async fn open(instance: &Instance, root_id: &ID) -> Result<Self> {
407 // Verify the root entry exists. Surfaces "database doesn't exist"
408 // at open time instead of at first read/write.
409 instance.backend().get(root_id).await?;
410
411 Ok(Self {
412 root: root_id.clone(),
413 instance: instance.downgrade(),
414 ops: instance.backend().clone(),
415 key: None,
416 allow_unverified: false,
417 })
418 }
419
420 /// Open a database for remote access through a service connection.
421 ///
422 /// Constructs a [`Database`] handle whose backing
423 /// [`Backend`](crate::instance::backend::Backend) is a
424 /// [`RemoteBackend`](crate::instance::backend::RemoteBackend) bound to
425 /// `identity`, so every [`Transaction`]/[`Store`] read and write travels
426 /// over the connection as a `DatabaseOp` under that identity. The
427 /// `identity` must match the database's auth settings for the caller's
428 /// key. `Instance::connect` must be used to create the instance.
429 #[cfg(all(unix, feature = "service"))]
430 pub async fn open_remote(
431 instance: &Instance,
432 conn: RemoteConnection,
433 root_id: &ID,
434 identity: SigKey,
435 ) -> Result<Self> {
436 instance.backend().get(root_id).await?;
437 Ok(Self {
438 root: root_id.clone(),
439 instance: instance.downgrade(),
440 ops: Arc::new(RemoteBackend::new(conn, Some(identity))),
441 key: None,
442 allow_unverified: false,
443 })
444 }
445
446 /// Attach a signing key to this database handle.
447 ///
448 /// The key is stored for use by future transactions. No validation is
449 /// performed; invalid keys will cause errors at commit time or when
450 /// calling [`current_permission`](Self::current_permission).
451 ///
452 /// Calling `with_key` again replaces any previously-attached key — the
453 /// most recent call wins.
454 ///
455 /// To discover which `SigKey` identity to use for a given public key,
456 /// use [`Database::find_sigkeys`].
457 pub fn with_key(self, key: impl Into<DatabaseKey>) -> Self {
458 Self {
459 key: Some(key.into()),
460 ..self
461 }
462 }
463
464 /// Include `Unverified` entries in this handle's reads.
465 ///
466 /// By default a `Database` exposes only the **Verified frontier**: the
467 /// maximal prefix of the DAG (an ancestor-closed set, starting at the
468 /// root) in which every entry is `Verified`. Tips that are still
469 /// `Unverified` — and everything reachable only through them — are hidden,
470 /// so a default read never reflects state this node could not authenticate.
471 ///
472 /// Calling `allow_unverified` opts this handle into the looser view that
473 /// also includes `Unverified` entries (everything except `Failed`, which
474 /// is always dropped). Use it when you explicitly want to observe
475 /// not-yet-verified data — e.g. freshly synced entries whose pinned
476 /// `_settings` this node does not hold yet.
477 ///
478 /// This is a per-handle setting and composes with [`with_key`](Self::with_key):
479 ///
480 /// ```rust,no_run
481 /// # use eidetica::*;
482 /// # use eidetica::auth::crypto::generate_keypair;
483 /// # async fn example(instance: Instance, root_id: ID) -> Result<()> {
484 /// # let (signing_key, _) = generate_keypair();
485 /// let db = Database::open(&instance, &root_id)
486 /// .await?
487 /// .with_key(signing_key)
488 /// .allow_unverified();
489 /// # Ok(())
490 /// # }
491 /// ```
492 ///
493 /// # Note on CRDT coherence
494 ///
495 /// The Verified frontier is a *prefix* cut, not a per-value filter. An
496 /// interior `Unverified` entry hides all of its descendants from the
497 /// default view even if those descendants are themselves `Verified`,
498 /// because exposing them without their unverifiable ancestor would yield
499 /// an incoherent CRDT state. Run [`verify`](Self::verify) to promote the
500 /// blocking entry, or `allow_unverified` to read past it.
501 pub fn allow_unverified(self) -> Self {
502 Self {
503 allow_unverified: true,
504 ..self
505 }
506 }
507
508 /// Validate a `DatabaseKey` against this database's auth settings.
509 ///
510 /// Checks that:
511 /// 1. The signing key derives to the public key claimed by the identity
512 /// 2. The identity exists in the database's auth settings
513 ///
514 /// Returns the effective permission for the validated key. Callers wanting
515 /// to fail fast on an invalid key should call
516 /// [`current_permission`](Self::current_permission), which wraps this.
517 async fn validate_key(&self, key: &DatabaseKey) -> Result<Permission> {
518 let settings_store = self.get_settings().await?;
519 let auth_settings = settings_store.auth_snapshot().await?;
520 let actual_pubkey = key.public_key();
521 let instance = match key.identity() {
522 // Delegation resolution needs an Instance for cross-tree lookups;
523 // direct identities resolve from `auth_settings` alone.
524 SigKey::Delegation { .. } => Some(self.instance()?),
525 _ => None,
526 };
527 crate::auth::validation::permissions::resolve_identity_permission(
528 &actual_pubkey,
529 key.identity(),
530 &auth_settings,
531 instance.as_ref(),
532 )
533 .await
534 }
535
536 /// Find all SigKeys that a public key can use to access a database.
537 ///
538 /// This static helper method loads a database's authentication settings and returns
539 /// all possible SigKeys that can be used with the given public key. This is useful for
540 /// discovering authentication options before opening a database.
541 ///
542 /// Returns all matching SigKeys including:
543 /// - Specific key names where the pubkey matches
544 /// - Global permission if available
545 /// - Single-hop delegation paths (pubkey found in a directly delegated tree)
546 ///
547 /// The results are **sorted by permission level, highest first**, making it easy to
548 /// select the most privileged access available.
549 ///
550 /// # Arguments
551 /// * `instance` - Instance handle for storage and coordination
552 /// * `root_id` - Root entry ID of the database to check
553 /// * `pubkey` - Public key string (e.g., "Ed25519:abc123...") to look up
554 ///
555 /// # Returns
556 /// A vector of (SigKey, Permission) tuples, sorted by permission (highest first).
557 /// Returns empty vector if no valid access methods are found.
558 ///
559 /// # Errors
560 /// Returns an error if:
561 /// - Database cannot be loaded
562 /// - Auth settings cannot be parsed
563 ///
564 /// # Example
565 /// ```rust,no_run
566 /// # use eidetica::*;
567 /// # use eidetica::database::DatabaseKey;
568 /// # use eidetica::backend::database::InMemory;
569 /// # use eidetica::auth::crypto::generate_keypair;
570 /// # use eidetica::auth::types::SigKey;
571 /// # #[tokio::main]
572 /// # async fn main() -> Result<()> {
573 /// # let instance = Instance::open_backend(Box::new(InMemory::new())).await?;
574 /// # let (signing_key, pubkey) = generate_keypair();
575 /// # let root_id = ID::from_bytes(b"database_root_id");
576 /// // Find all SigKeys this pubkey can use (sorted highest permission first)
577 /// let sigkeys = Database::find_sigkeys(&instance, &root_id, &pubkey).await?;
578 ///
579 /// // Use the first available SigKey (highest permission)
580 /// if let Some((sigkey, _permission)) = sigkeys.first() {
581 /// let key = DatabaseKey::with_identity(signing_key, sigkey.clone());
582 /// let database = Database::open(&instance, &root_id).await?.with_key(key);
583 /// }
584 /// # Ok(())
585 /// # }
586 /// ```
587 pub async fn find_sigkeys(
588 instance: &Instance,
589 root_id: &ID,
590 pubkey: &PublicKey,
591 ) -> Result<Vec<(SigKey, Permission)>> {
592 use crate::auth::{permission::clamp_permission, types::DelegationStep};
593
594 // Create temporary database to load settings (no key source needed for reading)
595 let temp_db = Self::open(instance, root_id).await?;
596
597 // Load auth settings
598 let settings_store = temp_db.get_settings().await?;
599 let auth_settings = settings_store.auth_snapshot().await?;
600
601 // Find direct SigKeys for this pubkey
602 let mut results = auth_settings.find_all_sigkeys_for_pubkey(pubkey);
603
604 // Scan single-hop delegation paths
605 // FIXME: deep nested delegations can't use this
606 if let Ok(delegated_trees) = auth_settings.get_all_delegated_trees() {
607 for (delegated_root_id, delegated_tree_ref) in &delegated_trees {
608 // Load the delegated tree's auth settings
609 let delegated_db = match Self::open(instance, delegated_root_id).await {
610 Ok(db) => db,
611 Err(_) => continue,
612 };
613 let delegated_settings = match delegated_db.get_settings().await {
614 Ok(s) => s,
615 Err(_) => continue,
616 };
617 let delegated_auth = match delegated_settings.auth_snapshot().await {
618 Ok(a) => a,
619 Err(_) => continue,
620 };
621
622 // Check if pubkey exists in the delegated tree
623 let delegated_sigkeys = delegated_auth.find_all_sigkeys_for_pubkey(pubkey);
624 if delegated_sigkeys.is_empty() {
625 continue;
626 }
627
628 // Get current tips for the delegated tree
629 let tips = match instance.backend().snapshot(delegated_root_id).await {
630 Ok(snap) => snap.into_tips(),
631 Err(_) => continue,
632 };
633
634 // For each matching key in the delegated tree, construct a delegation SigKey
635 for (delegated_sk, delegated_perm) in delegated_sigkeys {
636 // Clamp the delegated permission through the bounds
637 let effective_perm =
638 clamp_permission(delegated_perm, &delegated_tree_ref.permission_bounds);
639
640 // Construct the delegation SigKey using the hint from the direct key
641 let delegation_sigkey = SigKey::Delegation {
642 path: vec![DelegationStep {
643 tree: delegated_root_id.clone(),
644 tips: tips.clone(),
645 }],
646 hint: delegated_sk.hint().clone(),
647 };
648
649 results.push((delegation_sigkey, effective_perm));
650 }
651 }
652 }
653
654 // Sort by permission, highest first
655 results.sort_by_key(|b| std::cmp::Reverse(b.1));
656 Ok(results)
657 }
658
659 /// Get the auth identity for this database's configured key.
660 pub fn auth_identity(&self) -> Option<&SigKey> {
661 self.key.as_ref().map(|k| &k.identity)
662 }
663
664 /// Register a callback to be invoked when entries are written to this database.
665 ///
666 /// The callback fires for **both** local writes (transaction commits) and remote
667 /// writes (sync). Branch on [`WriteEvent::source`](crate::WriteEvent::source) inside
668 /// the closure if you only care about one.
669 ///
670 /// Returns a [`WriteCallback`] handle. **Drop it to unregister.** Call
671 /// [`WriteCallback::detach`] to leave the callback registered for the life
672 /// of the [`Instance`] without holding the handle.
673 ///
674 /// **Important:** Callbacks are registered at the Instance level and fire for all
675 /// writes to the database tree (identified by root ID), regardless of which
676 /// `Database` handle performed the write or registered the callback.
677 ///
678 /// # ⚠️ Limited semantics on a connected (remote) [`Instance`]
679 ///
680 /// On a daemon-backed [`Instance`], `on_write` is **best-effort and partial**.
681 /// It only observes writes whose commit ran through this client's
682 /// [`Instance::put_entry`]. The following writes are **not** observed:
683 ///
684 /// - Commits made by other client processes connected to the same daemon.
685 /// - Entries the daemon receives via sync from peers.
686 /// - Anything the daemon writes outside this client's commit path.
687 ///
688 /// In addition, [`WriteEvent::previous_tips`] is **always empty** for writes
689 /// that go through a remote backend — the canonical DAG lives on the daemon
690 /// and the client has nothing local to read tips from. Callbacks that diff
691 /// `previous_tips` against `event.entries()` will see "the world was empty"
692 /// on every event.
693 ///
694 /// If you need full cross-client / cross-sync notification semantics, use a
695 /// local [`Instance`] for now. A server-push subscription path is planned —
696 /// when it lands, this method will gain the same contract on remote.
697 ///
698 /// # Callback contract (local [`Instance`])
699 ///
700 /// - **Local writes**: fires once per transaction commit; the [`WriteEvent`]
701 /// contains exactly one entry.
702 /// - **Remote writes**: fires once per sync batch (not per entry); the
703 /// [`WriteEvent`] may contain multiple entries received together.
704 /// - All entries in the event are fully persisted before the callback fires.
705 /// - [`WriteEvent::previous_tips`] contains the DAG tips from before the
706 /// write, so consumers can determine exactly what changed.
707 /// - Errors are logged but do not prevent other callbacks from running.
708 /// - The `db` argument is a **read-only** [`Database`] handle (no
709 /// [`DatabaseKey`] configured): you can read settings, entries, and
710 /// metadata, but cannot commit transactions through it. To write from
711 /// inside a callback, resolve through `db.instance()?` and open the
712 /// database with the appropriate key.
713 /// - **Reentrance**: writes are serialized per-tree via an async lock
714 /// that is held while callbacks run. A callback must not commit a
715 /// transaction on the same tree it was invoked for — that would
716 /// deadlock. Spawn a task or write to a different tree instead.
717 ///
718 /// # Example
719 /// ```rust,no_run
720 /// # use eidetica::*;
721 /// # use eidetica::crdt::Doc;
722 /// # use eidetica::backend::database::InMemory;
723 /// # use eidetica::auth::crypto::PrivateKey;
724 /// # #[tokio::main]
725 /// # async fn main() -> Result<()> {
726 /// let instance = Instance::open_backend(Box::new(InMemory::new())).await?;
727 /// # let signing_key = PrivateKey::generate();
728 /// # let database = Database::create(&instance, signing_key, Doc::new()).await?;
729 ///
730 /// let cb = database.on_write(|event, db| {
731 /// let count = event.entries().len();
732 /// let source = event.source();
733 /// let db_id = db.root_id().clone();
734 /// async move {
735 /// println!("{count} entries written to {db_id} ({source:?})");
736 /// Ok(())
737 /// }
738 /// })?;
739 ///
740 /// // Drop `cb` to unregister, or:
741 /// cb.detach(); // keep registered for the life of the Instance
742 /// # Ok(())
743 /// # }
744 /// ```
745 ///
746 /// If a callback needs the [`Instance`], call [`Database::instance`] on
747 /// the `db` argument.
748 pub fn on_write<F, Fut>(&self, callback: F) -> Result<WriteCallback>
749 where
750 F: for<'a> Fn(&'a WriteEvent, &'a Database) -> Fut + Send + std::marker::Sync + 'static,
751 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
752 {
753 let instance = self.instance()?;
754 let tree_id = self.root_id().clone();
755 let id = instance.register_write_callback(tree_id.clone(), callback);
756 Ok(WriteCallback::new_per_database(
757 instance.downgrade(),
758 tree_id,
759 id,
760 ))
761 }
762
763 /// Get the ID of the root entry
764 pub fn root_id(&self) -> &ID {
765 &self.root
766 }
767
768 /// Upgrade the weak instance reference to a strong reference.
769 ///
770 /// `Database` holds a [`WeakInstance`](crate::WeakInstance), so this can
771 /// fail if the owning [`Instance`] has already been dropped.
772 pub fn instance(&self) -> Result<Instance> {
773 self.instance
774 .upgrade()
775 .ok_or_else(|| Error::Instance(Box::new(InstanceError::InstanceDropped)))
776 }
777
778 /// Get a clone of the backend seam.
779 pub fn backend(&self) -> Result<Arc<dyn Backend>> {
780 Ok(self.instance()?.backend().clone())
781 }
782
783 /// The storage seam this handle's `Transaction`/`Store` reads/writes flow
784 /// through (a [`LocalBackend`](crate::instance::backend::LocalBackend) clone
785 /// of the instance's backend, or a per-handle
786 /// [`RemoteBackend`](crate::instance::backend::RemoteBackend)).
787 pub(crate) fn ops(&self) -> &dyn Backend {
788 self.ops.as_ref()
789 }
790
791 /// Retrieve the root entry from the backend
792 pub async fn get_root(&self) -> Result<Entry> {
793 let instance = self.instance()?;
794 instance.get(&self.root).await
795 }
796
797 /// Get a read-only settings store for the database.
798 ///
799 /// Returns a SettingsStore that provides access to the database's settings.
800 /// Since this creates an internal transaction that is never committed, any
801 /// modifications made through the returned store will not persist.
802 ///
803 /// For making persistent changes to settings, create a transaction and use
804 /// `Transaction::get_settings()` instead.
805 ///
806 /// # Returns
807 /// A `Result` containing the `SettingsStore` for settings or an error.
808 ///
809 /// # Example
810 /// ```rust,no_run
811 /// # use eidetica::Database;
812 /// # async fn example(database: Database) -> eidetica::Result<()> {
813 /// // Read-only access
814 /// let settings = database.get_settings().await?;
815 /// let name = settings.get_name().await?;
816 ///
817 /// // For modifications, use a transaction:
818 /// let txn = database.new_transaction().await?;
819 /// let settings = txn.get_settings()?;
820 /// settings.set_name("new_name").await?;
821 /// txn.commit().await?;
822 /// # Ok(())
823 /// # }
824 /// ```
825 pub async fn get_settings(&self) -> Result<SettingsStore> {
826 let txn = self.new_transaction().await?;
827 txn.get_settings()
828 }
829
830 /// Get the name of the database from its settings store
831 pub async fn get_name(&self) -> Result<String> {
832 let settings = self.get_settings().await?;
833 settings.get_name().await
834 }
835
836 /// Create a new atomic transaction on this database
837 ///
838 /// This creates a new atomic transaction containing a new Entry.
839 /// The atomic transaction will be initialized with the current state of the database.
840 /// If a default authentication key is set, the transaction will use it for signing.
841 ///
842 /// # Returns
843 /// A `Result<Transaction>` containing the new atomic transaction
844 pub async fn new_transaction(&self) -> Result<Transaction> {
845 let snapshot = self.snapshot().await?;
846 self.new_transaction_at(&snapshot).await
847 }
848
849 /// Create a new atomic transaction on this database anchored at a specific snapshot.
850 ///
851 /// The transaction's parents are taken from the provided snapshot's tips instead of
852 /// the database's current state. This allows creating complex DAG structures
853 /// like diamond patterns for testing and advanced use cases.
854 ///
855 /// # Arguments
856 /// * `snapshot` - The snapshot to anchor the transaction at
857 ///
858 /// # Returns
859 /// A `Result<Transaction>` containing the new atomic transaction
860 pub async fn new_transaction_at(&self, snapshot: &Snapshot) -> Result<Transaction> {
861 let mut txn = Transaction::new_at(self, snapshot).await?;
862
863 // Set provided signing key from DatabaseKey
864 if let Some(key) = &self.key {
865 txn.set_provided_key(*key.signing_key.clone(), key.identity.clone());
866 }
867
868 Ok(txn)
869 }
870
871 /// Gather everything a client needs to build and sign a transaction
872 /// locally for the given stores, with parents drawn from `scope`'s
873 /// projection.
874 ///
875 /// This is **single-sourced**: both the server's `BeginTransaction`
876 /// handler and the Phase-3 remote seam call it, so
877 /// `Transaction::commit`'s build-sign path has one source of truth for
878 /// context gathering.
879 ///
880 /// `scope=AllowUnverified` opens against the raw DAG (only `Failed`
881 /// dropped); the default `Verified` scope uses the Verified frontier.
882 /// The returned [`TransactionContext`] carries everything needed for
883 /// one round-trip transaction build: main parents + heights, per-store
884 /// subtree parents + heights, settings tips, and the merged `_settings`
885 /// CRDT state this entry is authored against.
886 #[cfg(all(unix, feature = "service"))]
887 pub async fn transaction_context(
888 &self,
889 stores: &[String],
890 scope: crate::service::protocol::ReadScope,
891 ) -> Result<crate::service::protocol::TransactionContext> {
892 use crate::service::protocol::{ReadScope, TransactionContext};
893
894 // -- scope-sensitive main tips --------------------------------
895 let db_for_tips = Database {
896 allow_unverified: matches!(scope, ReadScope::AllowUnverified),
897 ..self.clone()
898 };
899 let main_snapshot = db_for_tips.snapshot().await?;
900 let main_tips = main_snapshot.tips();
901
902 // -- main parents: (tip, height) ------------------------------
903 let mut main_parents = Vec::with_capacity(main_tips.len());
904 for tip in main_tips {
905 let entry = self.ops().get(tip).await?;
906 main_parents.push((tip.clone(), entry.height()));
907 }
908
909 // -- per-store subtree parents: (tip, subtree_height) ---------
910 let mut subtree_parents = std::collections::BTreeMap::new();
911 for store in stores {
912 let child_snap = self
913 .ops()
914 .store_snapshot_at(self.root_id(), store, &main_snapshot)
915 .await?;
916 let mut pairs = Vec::with_capacity(child_snap.len());
917 for tip in child_snap.tips() {
918 let entry = self.ops().get(tip).await?;
919 let height = entry.subtree_height(store).unwrap_or(0);
920 pairs.push((tip.clone(), height));
921 }
922 subtree_parents.insert(store.clone(), pairs);
923 }
924
925 // -- settings tips (pinned in entry metadata) -----------------
926 let settings_tips = self
927 .ops()
928 .store_snapshot_at(self.root_id(), SETTINGS, &main_snapshot)
929 .await?
930 .into_tips();
931
932 // -- merged _settings state as serde_json::Value --------------
933 let txn = Transaction::new_at(self, &main_snapshot).await?;
934 let settings_doc: Doc = txn.get_full_state(SETTINGS).await?;
935 let settings_value = serde_json::to_value(&settings_doc)?;
936
937 Ok(TransactionContext {
938 main_parents,
939 subtree_parents,
940 settings_tips,
941 settings_value,
942 })
943 }
944
945 /// Server-materialized merged state of an **unencrypted** store, as a
946 /// `serde_json::Value` against the database's Verified frontier.
947 ///
948 /// Creates an ephemeral transaction, deserializes every entry's
949 /// store data as [`Doc`], and merges them via Doc's LWW merge —
950 /// the same merge `Store<T>` would perform client-side. All current
951 /// store types (DocStore, Table, Settings) serialize their data as
952 /// JSON, so `Doc`-typed deserialization works universally.
953 ///
954 /// # Encrypted stores
955 ///
956 /// Encrypted stores cannot be materialized this way (the ephemeral
957 /// transaction has no encryptor, so `serde_json::from_slice::<Doc>`
958 /// would fail on ciphertext). The caller must use
959 /// [`get_store_entries`](Self::get_store_entries) for encrypted
960 /// stores and decrypt+merge client-side.
961 pub async fn get_store_state(&self, store: &str) -> Result<serde_json::Value> {
962 let txn = self.new_transaction().await?;
963 let state: Doc = txn.get_full_state(store).await?;
964 Ok(serde_json::to_value(&state)?)
965 }
966
967 /// Ordered (by subtree height), verifiable, opaque store entries
968 /// reachable from `tips` within `scope`.
969 ///
970 /// This is the **universal** primitive — works for encrypted and
971 /// unencrypted stores alike because it returns raw [`Entry`] records
972 /// with opaque [`RawData`](crate::entry::RawData); no deserialization
973 /// or merge runs server-side. The per-subtree-height ordering
974 /// (ascending, then by ID for tiebreaking) is exactly the canonical
975 /// CRDT replay order produced by
976 /// [`sort_entries_by_subtree_height`](crate::backend::database::in_memory::cache::sort_entries_by_subtree_height).
977 ///
978 /// When `scope` is [`ReadScope::Verified`] and `tips` are the
979 /// Verified-frontier tips from [`snapshot`](Self::snapshot),
980 /// every returned entry is guaranteed `Verified` (the frontier is
981 /// ancestor-closed). For [`ReadScope::AllowUnverified`], entries
982 /// reachable from unverified tips are included.
983 #[cfg(all(unix, feature = "service"))]
984 pub async fn get_store_entries(
985 &self,
986 store: &str,
987 tips: &[ID],
988 _scope: crate::service::protocol::ReadScope,
989 ) -> Result<Vec<Entry>> {
990 let snapshot = Snapshot::from(tips.to_vec());
991 self.ops().store_at(self.root_id(), store, &snapshot).await
992 }
993
994 /// Execute a closure within a transaction and commit the result.
995 ///
996 /// This is a convenience wrapper for the common pattern of creating a transaction,
997 /// performing store operations, and committing. The transaction is committed after
998 /// the closure returns `Ok`. If the closure returns `Err`, the transaction is
999 /// dropped without committing.
1000 ///
1001 /// For read-only access, use [`get_store_viewer`](Self::get_store_viewer) instead.
1002 ///
1003 /// # Arguments
1004 /// * `f` - A closure that receives the [`Transaction`] and performs store operations.
1005 /// The closure should return `Ok(R)` on success.
1006 ///
1007 /// # Returns
1008 /// On success, returns the value produced by the closure after committing.
1009 /// The commit ID is not returned; use [`new_transaction`](Self::new_transaction)
1010 /// directly if you need it.
1011 ///
1012 /// # Errors
1013 /// Returns an error if transaction creation, the closure, or commit fails.
1014 /// If the closure fails, the transaction is not committed.
1015 ///
1016 /// # Example
1017 /// ```rust,no_run
1018 /// # use eidetica::*;
1019 /// # use eidetica::store::Table;
1020 /// # use serde::{Serialize, Deserialize};
1021 /// # #[derive(Clone, Serialize, Deserialize)]
1022 /// # struct Todo { title: String }
1023 /// # async fn example(db: Database) -> Result<()> {
1024 /// // Insert a record and get its generated key
1025 /// let key = db.with_transaction(|txn| async move {
1026 /// let store = txn.get_store::<Table<Todo>>("todos").await?;
1027 /// store.insert(Todo { title: "Buy milk".into() }).await
1028 /// }).await?;
1029 ///
1030 /// // Multiple operations in one atomic transaction
1031 /// db.with_transaction(|txn| async move {
1032 /// let store = txn.get_store::<Table<Todo>>("todos").await?;
1033 /// store.insert(Todo { title: "First".into() }).await?;
1034 /// store.insert(Todo { title: "Second".into() }).await?;
1035 /// Ok(())
1036 /// }).await?;
1037 /// # Ok(())
1038 /// # }
1039 /// ```
1040 pub async fn with_transaction<F, Fut, R>(&self, f: F) -> Result<R>
1041 where
1042 F: FnOnce(Transaction) -> Fut + Send,
1043 Fut: Future<Output = Result<R>> + Send,
1044 {
1045 let txn = self.new_transaction().await?;
1046 let commit_handle = txn.clone();
1047 let result = f(txn).await?;
1048 commit_handle.commit().await?;
1049 Ok(result)
1050 }
1051
1052 /// Insert an entry into the database without modifying or validating it.
1053 /// Primarily for testing / full control over raw entry storage.
1054 ///
1055 /// The entry is stored `Unverified`: this path runs no validation, so it
1056 /// cannot honestly claim the entry is verified, and the storage API no
1057 /// longer accepts a caller-asserted status. Only the local validation
1058 /// pass promotes entries to `Verified`.
1059 pub async fn insert_raw(&self, entry: Entry) -> Result<ID> {
1060 let instance = self.instance()?;
1061 let id = entry.id();
1062
1063 instance.put(entry).await?;
1064
1065 Ok(id)
1066 }
1067
1068 /// Get a Store type that will handle accesses to the Store
1069 /// This will return a Store initialized to point at the current state of the database.
1070 ///
1071 /// The returned store should NOT be used to modify the database, as it intentionally does not
1072 /// expose the Transaction. Since the Transaction is never committed, it does not have any
1073 /// effect on the database.
1074 pub async fn get_store_viewer<T>(&self, name: impl Into<String>) -> Result<T>
1075 where
1076 T: Store,
1077 {
1078 let txn = self.new_transaction().await?;
1079 T::load(&txn, name.into()).await
1080 }
1081
1082 /// Get the current tips (leaf entries) of the main database branch.
1083 ///
1084 /// Tips represent the latest entries in the database's main history, forming the heads of the DAG.
1085 ///
1086 /// If any raw tip is `Unverified`, an opportunistic [`Self::verify`] pass
1087 /// runs first (entries arrive `Unverified` from sync; this promotes the
1088 /// ones whose pinned `_settings` are now held).
1089 ///
1090 /// The returned tips then depend on the handle's view:
1091 ///
1092 /// - **default** — the **Verified frontier**: the tips of the maximal
1093 /// ancestor-closed, all-`Verified` prefix of the DAG. A still-`Unverified`
1094 /// tip is replaced by its nearest `Verified` ancestors; anything reachable
1095 /// only through an `Unverified` entry is excluded.
1096 /// - **[`allow_unverified`](Self::allow_unverified)** — the raw tips with
1097 /// only `Failed` entries dropped (`Unverified` tips are kept).
1098 ///
1099 /// `Failed` entries are dropped in both cases. While a [`verify`](Self::verify)
1100 /// pass is on the stack the frontier is bypassed (its own reads must see the
1101 /// raw DAG to reconstruct pinned `_settings`); a remote backend returns its
1102 /// raw tips unchanged (the server owns verification).
1103 ///
1104 /// # Returns
1105 /// A `Result` containing the [`Snapshot`] of tip entries or an error.
1106 pub async fn snapshot(&self) -> Result<Snapshot> {
1107 let instance = self.instance()?;
1108
1109 // On a remote instance the server owns verification: `snapshot`
1110 // already returns the server-side Verified frontier (or empty for a
1111 // not-yet-propagated tree, e.g. `Database::create`'s bootstrap
1112 // placeholder root — `EntryNotFound` is mapped to empty to match
1113 // `Backend::snapshot`'s contract). Return it directly: the local
1114 // verification machinery below (status probe, auto-verify,
1115 // `verified_frontier`) is local-only and would fail on a remote
1116 // backend anyway (e.g. `verified_frontier`'s `backend.get_tree(...)`).
1117 //
1118 // Delegate to `self.ops()` rather than calling the connection
1119 // directly: when this handle was built via `Database::create` or
1120 // `Database::open_remote` its `ops` is a `RemoteBackend` carrying the
1121 // *per-database* identity (the new tree's own member key, or the
1122 // caller's chosen identity), which the server's per-tree gate accepts.
1123 // Routing through `conn.session_identity()` here would instead use the
1124 // connection's (caller's) session pubkey, which is not a member of a
1125 // freshly-created tree and gets denied. A handle from `Database::open`
1126 // on a connected instance instead clones the instance's session
1127 // backend, keeping the session-identity semantics for that path.
1128 #[cfg(all(unix, feature = "service"))]
1129 if instance.remote_connection().is_some() {
1130 return match self.ops().snapshot(&self.root).await {
1131 Ok(snap) => Ok(snap),
1132 Err(e) if e.is_not_found() => Ok(Snapshot::EMPTY),
1133 Err(e) => Err(e),
1134 };
1135 }
1136
1137 // Local path: verification-status probing needs the concrete engine.
1138 let backend = instance.require_local_engine()?;
1139 let tips = self.ops().snapshot(&self.root).await?.into_tips();
1140
1141 // Verification status ops are local-only. On a remote backend the
1142 // server owns verification (and stores everything Unverified until
1143 // it verifies); the client returns verified tips unchanged.
1144 if let Some(first) = tips.first()
1145 && backend.get_verification_status(first).await.is_err()
1146 {
1147 return Ok(Snapshot::new(tips));
1148 }
1149
1150 // Access-time opportunistic verification: if any tip is still
1151 // Unverified, attempt to resolve it now. Best-effort — a failure or a
1152 // still-incomplete pin must not block the read. Suppressed while a
1153 // verify pass is already on the stack (its own reads land here).
1154 let tips = if auto_verify_suppressed() {
1155 tips
1156 } else {
1157 let mut any_unverified = false;
1158 for t in &tips {
1159 if backend
1160 .get_verification_status(t)
1161 .await
1162 .unwrap_or(VerificationStatus::Unverified)
1163 == VerificationStatus::Unverified
1164 {
1165 any_unverified = true;
1166 break;
1167 }
1168 }
1169 if any_unverified {
1170 // Boxed: this call closes a snapshot → verify →
1171 // validate_entry → delegation → get_settings → snapshot
1172 // async cycle; the box gives it a finite future size.
1173 let _ = Box::pin(self.verify()).await;
1174 self.ops().snapshot(&self.root).await?.into_tips()
1175 } else {
1176 tips
1177 }
1178 };
1179
1180 // Default view: cut to the Verified frontier. Suppressed while a
1181 // verify pass is on the stack — its reads must see the raw DAG to
1182 // reconstruct pinned `_settings` (the frontier filter itself depends
1183 // on verification status, which is exactly what verify is computing).
1184 if !self.allow_unverified && !auto_verify_suppressed() {
1185 return self.verified_frontier().await.map(Snapshot::new);
1186 }
1187
1188 // `allow_unverified` view: keep Unverified tips, drop only Failed.
1189 let mut visible = Vec::with_capacity(tips.len());
1190 for t in tips {
1191 if backend.get_verification_status(&t).await? != VerificationStatus::Failed {
1192 visible.push(t);
1193 }
1194 }
1195 Ok(Snapshot::new(visible))
1196 }
1197
1198 /// Compute the tips of the maximal all-`Verified` prefix of the DAG.
1199 ///
1200 /// An entry is in the prefix iff it is `Verified` **and** every one of its
1201 /// parents is in the prefix (the prefix is ancestor-closed). The frontier
1202 /// is the set of prefix entries that are not the parent of any other
1203 /// prefix entry — i.e. the tips of the verified subgraph.
1204 ///
1205 /// Returns an empty vector if the root itself is not `Verified` (nothing
1206 /// is observable in the default view until verification reaches the root).
1207 async fn verified_frontier(&self) -> Result<Vec<ID>> {
1208 let instance = self.instance()?;
1209 let backend = instance.require_local_engine()?;
1210
1211 // Topologically sorted (height then ID): every parent precedes its
1212 // children, so a single forward pass can decide prefix membership.
1213 let entries = backend.get_tree(self.root_id()).await?;
1214
1215 let mut in_prefix: std::collections::HashSet<ID> = std::collections::HashSet::new();
1216 let mut covered: std::collections::HashSet<ID> = std::collections::HashSet::new();
1217
1218 for e in &entries {
1219 let id = e.id();
1220 if backend.get_verification_status(&id).await? != VerificationStatus::Verified {
1221 continue;
1222 }
1223 let parents = e.parents().unwrap_or_default();
1224 if parents.iter().all(|p| in_prefix.contains(p)) {
1225 in_prefix.insert(id);
1226 // Every parent now has a verified child, so it is interior to
1227 // the prefix and cannot itself be a frontier tip.
1228 for p in parents {
1229 covered.insert(p);
1230 }
1231 }
1232 }
1233
1234 let frontier: Vec<ID> = entries
1235 .into_iter()
1236 .map(|e| e.id())
1237 .filter(|id| in_prefix.contains(id) && !covered.contains(id))
1238 .collect();
1239 Ok(frontier)
1240 }
1241
1242 /// Get the full `Entry` objects for the current tips of the main database branch.
1243 ///
1244 /// # Returns
1245 /// A `Result` containing a vector of the tip `Entry` objects or an error.
1246 pub async fn get_tip_entries(&self) -> Result<Vec<Entry>> {
1247 let instance = self.instance()?;
1248 let snapshot = self.snapshot().await?;
1249 let mut entries = Vec::new();
1250 for id in snapshot.tips() {
1251 entries.push(instance.get(id).await?);
1252 }
1253 Ok(entries)
1254 }
1255
1256 /// Get a single entry by ID from this database.
1257 ///
1258 /// This is the primary method for retrieving entries after commit operations.
1259 /// It provides safe, high-level access to entry data without exposing backend details.
1260 ///
1261 /// The method verifies that the entry belongs to this database by checking its root ID.
1262 /// If the entry exists but belongs to a different database, an error is returned.
1263 ///
1264 /// # Arguments
1265 /// * `entry_id` - The ID of the entry to retrieve (accepts anything that converts to ID/String)
1266 ///
1267 /// # Returns
1268 /// A `Result` containing the `Entry` or an error if not found or not part of this database
1269 ///
1270 /// # Example
1271 /// ```rust,no_run
1272 /// # use eidetica::*;
1273 /// # use eidetica::Instance;
1274 /// # use eidetica::backend::database::InMemory;
1275 /// # use eidetica::crdt::Doc;
1276 /// # #[tokio::main]
1277 /// # async fn main() -> Result<()> {
1278 /// # let (_instance, mut user) = Instance::create_backend(
1279 /// # Box::new(InMemory::new()),
1280 /// # NewUser::passwordless("test"),
1281 /// # ).await?;
1282 /// # let key_id = user.add_private_key(None).await?;
1283 /// # let tree = user.create_database(Doc::new(), &key_id).await?;
1284 /// # let txn = tree.new_transaction().await?;
1285 /// let entry_id = txn.commit().await?;
1286 /// let entry = tree.get_entry(&entry_id).await?; // Using &ID
1287 /// let entry = tree.get_entry(entry_id.clone()).await?; // Using ID
1288 /// println!("Entry signature: {:?}", entry.sig);
1289 /// # Ok(())
1290 /// # }
1291 /// ```
1292 pub async fn get_entry<I: Into<ID>>(&self, entry_id: I) -> Result<Entry> {
1293 let id = entry_id.into();
1294 // Route through `self.ops()` so handles built via `Database::create`
1295 // or `Database::open_remote` read with the per-DB identity from
1296 // their `RemoteBackend`. Going through `instance.get(id)` would
1297 // use the connection's login pubkey, which on a remote instance is
1298 // denied by the per-tree gate when the login key isn't a member of
1299 // this tree (e.g. user-tree key created via `User::add_private_key`
1300 // and used to author a database that doesn't grant the root key).
1301 let entry = self.ops().get(&id).await?;
1302
1303 // Check if the entry belongs to this database
1304 if !entry.in_tree(&self.root) {
1305 return Err(InstanceError::EntryNotInDatabase {
1306 entry_id: id,
1307 database_id: self.root.clone(),
1308 }
1309 .into());
1310 }
1311
1312 Ok(entry)
1313 }
1314
1315 /// Get multiple entries by ID efficiently.
1316 ///
1317 /// This method retrieves multiple entries more efficiently than multiple `get_entry()` calls
1318 /// by minimizing conversion overhead and pre-allocating the result vector.
1319 ///
1320 /// The method verifies that all entries belong to this database by checking their root IDs.
1321 /// If any entry exists but belongs to a different database, an error is returned.
1322 ///
1323 /// # Parameters
1324 /// * `entry_ids` - An iterable of entry IDs to retrieve
1325 ///
1326 /// # Returns
1327 /// A `Result` containing a vector of `Entry` objects or an error if any entry is not found or not part of this database
1328 ///
1329 /// # Example
1330 /// ```rust,no_run
1331 /// # use eidetica::*;
1332 /// # use eidetica::Instance;
1333 /// # use eidetica::backend::database::InMemory;
1334 /// # use eidetica::crdt::Doc;
1335 /// # #[tokio::main]
1336 /// # async fn main() -> Result<()> {
1337 /// # let (_instance, mut user) = Instance::create_backend(
1338 /// # Box::new(InMemory::new()),
1339 /// # NewUser::passwordless("test"),
1340 /// # ).await?;
1341 /// # let key_id = user.add_private_key(None).await?;
1342 /// # let tree = user.create_database(Doc::new(), &key_id).await?;
1343 /// let entry_ids = vec![ID::from_bytes("id1"), ID::from_bytes("id2")];
1344 /// let entries = tree.get_entries(entry_ids).await?;
1345 /// # Ok(())
1346 /// # }
1347 /// ```
1348 pub async fn get_entries<I, T>(&self, entry_ids: I) -> Result<Vec<Entry>>
1349 where
1350 I: IntoIterator<Item = T>,
1351 T: std::borrow::Borrow<ID>,
1352 {
1353 let ids: Vec<ID> = entry_ids.into_iter().map(|t| t.borrow().clone()).collect();
1354 let instance = self.instance()?;
1355 let mut entries = Vec::with_capacity(ids.len());
1356
1357 for id in ids {
1358 let entry = instance.get(&id).await?;
1359
1360 // Check if the entry belongs to this database
1361 if !entry.in_tree(&self.root) {
1362 return Err(InstanceError::EntryNotInDatabase {
1363 entry_id: id,
1364 database_id: self.root.clone(),
1365 }
1366 .into());
1367 }
1368
1369 entries.push(entry);
1370 }
1371
1372 Ok(entries)
1373 }
1374
1375 // === AUTHENTICATION HELPERS ===
1376
1377 /// Verify an entry's signature and authentication against the database's configuration that was valid at the time of entry creation.
1378 ///
1379 /// This method validates that:
1380 /// 1. The entry belongs to this database
1381 /// 2. The entry is properly signed with a key that was authorized in the database's authentication settings at the time the entry was created
1382 /// 3. The signature is cryptographically valid
1383 ///
1384 /// The method uses the entry's metadata to determine which authentication settings were active when the entry was signed,
1385 /// ensuring that entries remain valid even if keys are later revoked or settings change.
1386 ///
1387 /// # Arguments
1388 /// * `entry_id` - The ID of the entry to verify (accepts anything that converts to ID/String)
1389 ///
1390 /// # Returns
1391 /// A `Result` containing `true` if the entry is valid and properly authenticated, `false` if authentication fails
1392 ///
1393 /// # Errors
1394 /// Returns an error if:
1395 /// - The entry is not found
1396 /// - The entry does not belong to this database
1397 /// - The entry's metadata cannot be parsed
1398 /// - The historical authentication settings cannot be retrieved
1399 pub async fn verify_entry_signature<I: Into<ID>>(&self, entry_id: I) -> Result<bool> {
1400 let entry = self.get_entry(entry_id).await?;
1401
1402 // Validate against the `_settings` the entry pins, not current
1403 // settings — so a later key revocation cannot retroactively
1404 // invalidate (or validate) historical entries.
1405 match self.get_historical_settings_for_entry(&entry).await? {
1406 // We do not hold the pinned `_settings` set, so we cannot make a
1407 // verification decision: report not-verified rather than guess.
1408 PinnedSettings::Incomplete => Ok(false),
1409 PinnedSettings::Complete(auth_settings) => {
1410 let instance = self.instance()?;
1411 let mut validator = AuthValidator::new();
1412 validator
1413 .validate_entry(&entry, &auth_settings, Some(&instance))
1414 .await
1415 }
1416 }
1417 }
1418
1419 /// Get the permission level for this database's configured signing key.
1420 ///
1421 /// Returns the effective permission for the key that was configured when opening
1422 /// or creating this database. This uses the already-resolved identity stored in
1423 /// the database's `DatabaseKey`.
1424 ///
1425 /// # Returns
1426 /// The effective Permission for the configured signing key.
1427 ///
1428 /// # Errors
1429 /// Returns an error if:
1430 /// - No signing key is configured (database opened without authentication)
1431 /// - The database settings cannot be retrieved
1432 /// - The key is no longer valid in the current auth settings
1433 ///
1434 /// # Example
1435 /// ```rust,no_run
1436 /// # use eidetica::*;
1437 /// # use eidetica::crdt::Doc;
1438 /// # use eidetica::backend::database::InMemory;
1439 /// # use eidetica::auth::crypto::generate_keypair;
1440 /// # #[tokio::main]
1441 /// # async fn main() -> Result<()> {
1442 /// # let instance = Instance::open_backend(Box::new(InMemory::new())).await?;
1443 /// # let (signing_key, _public_key) = generate_keypair();
1444 /// # let database = Database::create(&instance, signing_key, Doc::new()).await?;
1445 /// // Check if the current key has Admin permission
1446 /// let permission = database.current_permission().await?;
1447 /// if permission.can_admin() {
1448 /// println!("Current key has Admin permission!");
1449 /// }
1450 /// # Ok(())
1451 /// # }
1452 /// ```
1453 pub async fn current_permission(&self) -> Result<Permission> {
1454 let key = self
1455 .key
1456 .as_ref()
1457 .ok_or(AuthError::InvalidAuthConfiguration {
1458 reason: "No signing key configured for this database".to_string(),
1459 })?;
1460 self.validate_key(key).await
1461 }
1462
1463 /// Reconstruct the `_settings` auth config an entry's signature is pinned
1464 /// to, from the `settings_snapshot` recorded in its signed metadata.
1465 ///
1466 /// Validation must run against the settings the entry pinned — not the
1467 /// current settings — so granting authority later cannot retroactively
1468 /// invalidate an entry that pinned less, and (once revocation lands)
1469 /// removals are handled on a separate, current-settings path.
1470 ///
1471 /// Returns [`PinnedSettings::Incomplete`] when this node does not hold the
1472 /// full pinned `_settings` ancestor set; the caller must then leave the
1473 /// entry `Unverified` rather than guess against whatever it does hold.
1474 async fn get_historical_settings_for_entry(&self, entry: &Entry) -> Result<PinnedSettings> {
1475 let instance = self.instance()?;
1476 let backend = instance.backend();
1477
1478 // The pin: `_settings` snapshot recorded in the entry's signed metadata.
1479 let settings_tips: Vec<ID> = match entry.metadata() {
1480 Some(raw) => match serde_json::from_slice::<crate::transaction::EntryMetadata>(raw) {
1481 Ok(md) => md.settings_snapshot.into_tips(),
1482 // Unparsable metadata ⇒ we cannot establish the pin.
1483 Err(_) => return Ok(PinnedSettings::Incomplete),
1484 },
1485 None => Vec::new(),
1486 };
1487
1488 // Resolve the effective `_settings` tips to validate against.
1489 let effective_tips: Vec<ID> = if settings_tips.is_empty() {
1490 if entry.in_subtree(SETTINGS) {
1491 // Genesis / bootstrap: no prior `_settings` exists, so the
1492 // entry is self-authorising — validate against the auth it
1493 // itself establishes (TOFU), mirroring how the transaction
1494 // validates initial database creation. Seeding the
1495 // reconstruction with the entry itself folds in its own
1496 // `_settings` contribution.
1497 vec![entry.id()]
1498 } else {
1499 // No auth context at all (no settings ever configured) —
1500 // mirrors the transaction path's "auth never configured" case.
1501 return Ok(PinnedSettings::Complete(AuthSettings::new()));
1502 }
1503 } else {
1504 settings_tips
1505 };
1506
1507 // Completeness: every pinned tip and its full `_settings` ancestor
1508 // closure must be present locally. `store_at` silently
1509 // skips absent ancestors, so an explicit walk is required — a missing
1510 // ancestor would otherwise yield a wrong (partial) auth config.
1511 let mut stack: Vec<ID> = effective_tips.clone();
1512 let mut seen: std::collections::HashSet<ID> = std::collections::HashSet::new();
1513 while let Some(id) = stack.pop() {
1514 if !seen.insert(id.clone()) {
1515 continue;
1516 }
1517 let Ok(e) = backend.get(&id).await else {
1518 return Ok(PinnedSettings::Incomplete);
1519 };
1520 // Walk both the `_settings` subtree DAG and the main parents that
1521 // carry it, so the closure can't be short-circuited.
1522 for p in e.subtree_parents(SETTINGS).unwrap_or_default() {
1523 stack.push(p);
1524 }
1525 for p in e.parents().unwrap_or_default() {
1526 stack.push(p);
1527 }
1528 }
1529
1530 // Reconstruct the merged `_settings` Doc as of the pinned tips.
1531 // Entries come back root-first; `_settings` is a system subtree and
1532 // is never encrypted, so deserialize directly.
1533 let effective_snapshot = Snapshot::from(effective_tips.clone());
1534 let entries = backend
1535 .store_at(self.root_id(), SETTINGS, &effective_snapshot)
1536 .await?;
1537 let mut settings_doc = Doc::default();
1538 for e in &entries {
1539 if let Ok(data) = e.data(SETTINGS) {
1540 let part: Doc = serde_json::from_slice(data)?;
1541 settings_doc = settings_doc.merge(&part)?;
1542 }
1543 }
1544
1545 let auth_settings = match settings_doc.get("auth") {
1546 Some(crate::crdt::doc::Value::Doc(auth_doc)) => auth_doc.clone().into(),
1547 _ => AuthSettings::new(),
1548 };
1549 Ok(PinnedSettings::Complete(auth_settings))
1550 }
1551
1552 /// Attempt to verify every `Unverified` entry in this database.
1553 ///
1554 /// For each `Unverified` entry, reconstruct the `_settings` it pins
1555 /// (see [`Self::get_historical_settings_for_entry`]) and validate its
1556 /// signature + permissions against that:
1557 ///
1558 /// - an ancestor is `Failed` → this entry is `Failed` too (quarantine
1559 /// propagates down the branch);
1560 /// - an ancestor is still `Unverified`, or not held locally yet (partial
1561 /// sync) → left `Unverified` (retried once the ancestor verifies / the
1562 /// missing entry arrives);
1563 /// - pinned `_settings` not fully held locally → left `Unverified`
1564 /// (a later pass retries once the set syncs in);
1565 /// - signature + permissions valid → promoted to `Verified`;
1566 /// - definitively invalid → marked `Failed` (dropped from reads).
1567 ///
1568 /// Verification is **prefix-closed**: an entry is `Verified` only if its
1569 /// entire ancestor history is `Verified`. It is therefore impossible for a
1570 /// tip to be `Verified` while one of its ancestors is not, which is what
1571 /// makes the Verified set ancestor-closed (see [`Self::allow_unverified`]).
1572 ///
1573 /// Already-`Verified` entries are never demoted here; that is a separate,
1574 /// not-yet-built path. Local-only — verification is a per-node decision
1575 /// and is never delegated to a peer.
1576 pub async fn verify(&self) -> Result<VerifyReport> {
1577 // Suppress the access-time auto-verify hook for the whole pass:
1578 // validation reads the database (delegation → settings → tips) and
1579 // must not recurse back into verification.
1580 IN_VERIFY
1581 .scope(true, async move {
1582 let instance = self.instance()?;
1583 let backend = instance.require_local_engine()?;
1584
1585 // Raw tree walk (not `snapshot`) so traversal itself never
1586 // re-enters the hook even before the guard is observed.
1587 let entries = backend.get_tree(self.root_id()).await?;
1588 let mut report = VerifyReport::default();
1589
1590 for entry in &entries {
1591 let id = entry.id();
1592 if backend.get_verification_status(&id).await? != VerificationStatus::Unverified
1593 {
1594 continue;
1595 }
1596
1597 // Verification is prefix-closed: an entry's trust rests on
1598 // its entire history, so it can only be `Verified` if every
1599 // ancestor is. `get_tree` is topo-sorted (parents before
1600 // children), so each parent's status is already final for
1601 // this pass.
1602 //
1603 // - any ancestor `Failed` → the branch is tainted; this
1604 // entry is `Failed` too (quarantine propagates forward);
1605 // - any ancestor still `Unverified` → cannot trust this
1606 // entry yet; leave it for a later pass.
1607 let parents = entry.parents().unwrap_or_default();
1608 let mut compromised = false;
1609 let mut blocked = false;
1610 for p in &parents {
1611 match backend.get_verification_status(p).await {
1612 Ok(VerificationStatus::Verified) => {}
1613 Ok(VerificationStatus::Failed) => compromised = true,
1614 Ok(VerificationStatus::Unverified) => blocked = true,
1615 // Parent not held locally yet (partial sync): we
1616 // cannot establish the history, so this entry is
1617 // blocked until the parent arrives — not an error.
1618 Err(e) if e.is_not_found() => blocked = true,
1619 Err(e) => return Err(e),
1620 }
1621 }
1622 if compromised {
1623 backend
1624 .update_verification_status(&id, VerificationStatus::Failed)
1625 .await?;
1626 report.failed += 1;
1627 continue;
1628 }
1629 if blocked {
1630 report.still_unverified += 1;
1631 continue;
1632 }
1633
1634 match self.get_historical_settings_for_entry(entry).await? {
1635 PinnedSettings::Incomplete => report.still_unverified += 1,
1636 PinnedSettings::Complete(auth_settings) => {
1637 let mut validator = AuthValidator::new();
1638 let valid = validator
1639 .validate_entry(entry, &auth_settings, Some(&instance))
1640 .await
1641 .unwrap_or(false);
1642 let new_status = if valid {
1643 report.verified += 1;
1644 VerificationStatus::Verified
1645 } else {
1646 report.failed += 1;
1647 VerificationStatus::Failed
1648 };
1649 backend.update_verification_status(&id, new_status).await?;
1650 }
1651 }
1652 }
1653 Ok(report)
1654 })
1655 .await
1656 }
1657
1658 // === DATABASE QUERIES ===
1659
1660 /// Get all entries in this database.
1661 ///
1662 /// ⚠️ **Warning**: This method loads all entries into memory. Use with caution on large databases.
1663 /// Consider using `snapshot()` or `get_tip_entries()` for more efficient access patterns.
1664 ///
1665 /// # Returns
1666 /// A `Result` containing a vector of all `Entry` objects in the database
1667 pub async fn get_all_entries(&self) -> Result<Vec<Entry>> {
1668 let instance = self.instance()?;
1669 instance.require_local_engine()?.get_tree(&self.root).await
1670 }
1671}