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 rand::{Rng, RngCore, distributions::Alphanumeric};
9use serde_json;
10
11use crate::{
12 Error, Instance, Result, Transaction, WeakInstance,
13 auth::{
14 crypto::{PrivateKey, PublicKey},
15 errors::AuthError,
16 settings::AuthSettings,
17 types::{AuthKey, Permission, SigKey},
18 validation::AuthValidator,
19 },
20 backend::VerificationStatus,
21 constants::{ROOT, SETTINGS},
22 crdt::Doc,
23 entry::{Entry, ID},
24 instance::{WriteSource, backend::Backend, errors::InstanceError},
25 store::{SettingsStore, Store},
26};
27
28#[cfg(test)]
29mod tests;
30
31/// A signing key bound to its identity in a database's auth settings.
32///
33/// Pairs the cryptographic signing key with information about how to look up
34/// permissions in the database's auth configuration. The identity determines
35/// which entry in `_settings.auth` this key maps to.
36#[derive(Clone, Debug)]
37pub struct DatabaseKey {
38 signing_key: Box<PrivateKey>,
39 identity: SigKey,
40}
41
42impl DatabaseKey {
43 /// Identity = pubkey derived from signing key. Most common case.
44 pub fn new(signing_key: PrivateKey) -> Self {
45 let pubkey = signing_key.public_key();
46 Self {
47 signing_key: Box::new(signing_key),
48 identity: SigKey::from_pubkey(&pubkey),
49 }
50 }
51
52 /// Identity = explicit SigKey (name, global, delegation, etc.)
53 pub fn with_identity(signing_key: PrivateKey, identity: SigKey) -> Self {
54 Self {
55 signing_key: Box::new(signing_key),
56 identity,
57 }
58 }
59
60 /// Identity = global permission with actual pubkey embedded for verification.
61 pub fn global(signing_key: PrivateKey) -> Self {
62 let pubkey = signing_key.public_key();
63 Self {
64 signing_key: Box::new(signing_key),
65 identity: SigKey::global(&pubkey),
66 }
67 }
68
69 /// Identity = key name lookup.
70 pub fn with_name(signing_key: PrivateKey, name: impl Into<String>) -> Self {
71 Self {
72 signing_key: Box::new(signing_key),
73 identity: SigKey::from_name(name),
74 }
75 }
76
77 /// Get the signing key.
78 pub fn signing_key(&self) -> &PrivateKey {
79 &self.signing_key
80 }
81
82 /// Get the public key.
83 pub fn public_key(&self) -> PublicKey {
84 self.signing_key.public_key()
85 }
86
87 /// Get the identity used for auth settings lookup.
88 pub fn identity(&self) -> &SigKey {
89 &self.identity
90 }
91
92 /// Consume self and return the parts.
93 pub fn into_parts(self) -> (PrivateKey, SigKey) {
94 (*self.signing_key, self.identity)
95 }
96}
97
98/// Represents a collection of related entries, like a traditional database or a branch in a version control system.
99///
100/// Each `Database` is identified by the ID of its root `Entry` and manages the history of data
101/// associated with that root. It interacts with the underlying storage through the Instance handle.
102#[derive(Clone, Debug)]
103pub struct Database {
104 root: ID,
105 instance: WeakInstance,
106 /// Signing key bound to its auth identity for this database
107 key: Option<DatabaseKey>,
108}
109
110impl Database {
111 /// Creates a new `Database` instance with a user-provided signing key.
112 ///
113 /// This constructor creates a new database using a signing key that's already in memory
114 /// (e.g., from UserKeyManager), without requiring the key to be stored in the backend.
115 /// This is the preferred method for creating databases in a User context where keys
116 /// are managed separately from the backend.
117 ///
118 /// The created database will use a `DatabaseKey` for all subsequent operations,
119 /// meaning transactions will use the provided key directly rather than looking it up
120 /// from backend storage.
121 ///
122 /// # Auth Bootstrapping
123 ///
124 /// Auth is always bootstrapped with the signing key as `Admin(0)`. Passing auth
125 /// configuration in `initial_settings` is an error — additional keys must be added
126 /// via follow-up transactions after creation.
127 ///
128 /// # Arguments
129 /// * `instance` - Instance handle for storage and coordination
130 /// * `signing_key` - The signing key to use for the initial commit and subsequent operations.
131 /// This key should already be decrypted and ready to use. The public key is derived
132 /// automatically and used as the key identifier in auth settings.
133 /// * `initial_settings` - `Doc` CRDT containing the initial settings for the database.
134 /// Use `Doc::new()` for an empty settings document.
135 ///
136 /// # Returns
137 /// A `Result` containing the new `Database` instance configured with a `DatabaseKey`.
138 ///
139 /// # Example
140 /// ```rust,no_run
141 /// # use eidetica::*;
142 /// # use eidetica::backend::database::InMemory;
143 /// # use eidetica::auth::crypto::generate_keypair;
144 /// # use eidetica::crdt::Doc;
145 /// # #[tokio::main]
146 /// # async fn main() -> Result<()> {
147 /// let instance = Instance::open(Box::new(InMemory::new())).await?;
148 /// let (signing_key, _public_key) = generate_keypair();
149 ///
150 /// let mut settings = Doc::new();
151 /// settings.set("name", "my_database");
152 ///
153 /// // Create database with user-managed key (no backend storage needed)
154 /// let database = Database::create(&instance, signing_key, settings).await?;
155 ///
156 /// // All transactions automatically use the provided key
157 /// let tx = database.new_transaction().await?;
158 /// # Ok(())
159 /// # }
160 /// ```
161 pub async fn create(
162 instance: &Instance,
163 signing_key: PrivateKey,
164 initial_settings: Doc,
165 ) -> Result<Self> {
166 let mut initial_settings = initial_settings;
167 let pubkey = signing_key.public_key();
168
169 // Reject preconfigured auth — Database::create owns auth bootstrapping entirely.
170 if initial_settings.get("auth").is_some() {
171 return Err(Error::Auth(AuthError::InvalidAuthConfiguration {
172 reason: "initial_settings must not contain auth configuration; \
173 Database::create bootstraps auth with the signing key as Admin(0)"
174 .to_string(),
175 }));
176 }
177
178 // Bootstrap auth with the signing key as Admin(0)
179 let mut auth_settings = AuthSettings::new();
180 auth_settings.add_key(&pubkey, AuthKey::active(None, Permission::Admin(0)))?;
181 initial_settings.set("auth", auth_settings.as_doc().clone());
182
183 // Create the initial root entry using a temporary Database and Transaction.
184 // This placeholder ID should not exist in the backend, so get_tips will be empty.
185 let bootstrap_placeholder_id = format!(
186 "bootstrap_root_{}",
187 rand::thread_rng()
188 .sample_iter(&Alphanumeric)
189 .take(10)
190 .map(char::from)
191 .collect::<String>()
192 );
193
194 // Create temporary database for bootstrap with DatabaseKey.
195 // This allows the bootstrap transaction to use the provided key directly.
196 let temp_database_for_bootstrap = Database {
197 root: bootstrap_placeholder_id.clone().into(),
198 instance: instance.downgrade(),
199 key: Some(DatabaseKey::new(signing_key.clone())),
200 };
201
202 // Create the transaction - it will use the provided key automatically
203 let txn = temp_database_for_bootstrap.new_transaction().await?;
204
205 // IMPORTANT: For the root entry, we need to set the database root to empty string
206 // so that is_root() returns true and all_roots() can find it
207 txn.set_entry_root("")?;
208
209 // Populate the SETTINGS and ROOT subtrees for the very first entry
210 txn.update_subtree(SETTINGS, &serde_json::to_string(&initial_settings)?)
211 .await?;
212 txn.update_subtree(ROOT, &serde_json::to_string(&"".to_string())?)
213 .await?; // Standard practice for root entry's _root
214
215 // Add entropy to the entry metadata to ensure unique database IDs even with identical settings
216 txn.set_metadata_entropy(rand::thread_rng().next_u64())?;
217
218 // Commit the initial entry
219 let new_root_id = txn.commit().await?;
220
221 // Now create the real database with the new_root_id and DatabaseKey
222 Ok(Self {
223 root: new_root_id,
224 instance: instance.downgrade(),
225 key: Some(DatabaseKey::new(signing_key)),
226 })
227 }
228
229 /// Opens a database for read-only access, bypassing authentication validation.
230 ///
231 /// # Internal Use Only
232 ///
233 /// This method bypasses authentication validation and is intended for internal
234 /// operations that require reading database state (loading settings, checking
235 /// permissions, resolving delegations, etc.).
236 ///
237 /// These operations should only be performed by the server/instance administrator,
238 /// but we don't verify that yet. Future versions may add admin permission checks.
239 ///
240 /// # Behavior
241 ///
242 /// - No authentication validation is performed
243 /// - The resulting database has no key source, so commits will fail
244 /// - Used internally for system operations that need read access
245 ///
246 /// # Arguments
247 /// * `id` - The `ID` of the root entry.
248 /// * `instance` - Instance handle for storage and coordination
249 ///
250 /// # Returns
251 /// A `Result` containing the new `Database` instance or an error.
252 pub(crate) fn open_unauthenticated(id: ID, instance: &Instance) -> Result<Self> {
253 // TODO: Audit all usages of this function
254 Ok(Self {
255 root: id,
256 instance: instance.downgrade(),
257 key: None,
258 })
259 }
260
261 /// Opens an existing `Database` with a `DatabaseKey`.
262 ///
263 /// This constructor opens an existing database by its root ID and configures it to use
264 /// a `DatabaseKey` for all subsequent operations. This is used in the User
265 /// context where keys are managed by UserKeyManager and already decrypted in memory.
266 ///
267 /// # Key Management
268 ///
269 /// This constructor uses **user-managed keys**:
270 /// - The key is provided directly (e.g., from UserKeyManager)
271 /// - Uses `DatabaseKey` for all subsequent operations
272 /// - No backend key storage needed
273 ///
274 /// Note: To **create** a new database with user-managed keys, use `create()`.
275 /// This method is for **opening existing** databases.
276 ///
277 /// To discover which SigKey to use for a given public key, use `Database::find_sigkeys()`.
278 ///
279 /// # Arguments
280 /// * `instance` - Instance handle for storage and coordination
281 /// * `root_id` - The root entry ID of the existing database to open
282 /// * `key` - A `DatabaseKey` combining signing key and auth identity
283 ///
284 /// # Returns
285 /// A `Result` containing the `Database` instance configured with the `DatabaseKey`
286 ///
287 /// # Example
288 /// ```rust,no_run
289 /// # use eidetica::*;
290 /// # use eidetica::database::DatabaseKey;
291 /// # use eidetica::backend::database::InMemory;
292 /// # use eidetica::auth::crypto::generate_keypair;
293 /// # #[tokio::main]
294 /// # async fn main() -> Result<()> {
295 /// # let instance = Instance::open(Box::new(InMemory::new())).await?;
296 /// # let (signing_key, _verifying_key) = generate_keypair();
297 /// # let root_id = "existing_database_root_id".into();
298 /// // Open database with pubkey identity (most common)
299 /// let key = DatabaseKey::new(signing_key);
300 /// let database = Database::open(instance, &root_id, key).await?;
301 ///
302 /// // All transactions automatically use the provided key
303 /// let tx = database.new_transaction().await?;
304 /// # Ok(())
305 /// # }
306 /// ```
307 pub async fn open(instance: Instance, root_id: &ID, key: DatabaseKey) -> Result<Self> {
308 let temp_db = Self::open_unauthenticated(root_id.clone(), &instance)?;
309 temp_db.validate_key(&key).await?;
310
311 Ok(Self {
312 root: root_id.clone(),
313 instance: instance.downgrade(),
314 key: Some(key),
315 })
316 }
317
318 /// Validate a `DatabaseKey` against this database's auth settings.
319 ///
320 /// Checks that:
321 /// 1. The signing key derives to the public key claimed by the identity
322 /// 2. The identity exists in the database's auth settings
323 ///
324 /// Returns the effective permission for the validated key.
325 ///
326 /// Used by `Database::open` (on an unauthenticated temp db) to fail fast
327 /// on invalid keys, and by `current_permission` to look up permissions.
328 async fn validate_key(&self, key: &DatabaseKey) -> Result<Permission> {
329 let settings_store = self.get_settings().await?;
330 let auth_settings = settings_store.auth_snapshot().await?;
331
332 // Derive actual pubkey from the signing key
333 let actual_pubkey = key.public_key();
334
335 match key.identity() {
336 SigKey::Direct(hint) if hint.is_global() => {
337 // Verify the embedded pubkey matches the actual signing key
338 if let Some(embedded_pubkey) = &hint.pubkey
339 && *embedded_pubkey != actual_pubkey
340 {
341 return Err(Error::Auth(AuthError::SigningKeyMismatch {
342 reason: format!(
343 "signing key derives pubkey '{actual_pubkey}' \
344 but global identity claims '{embedded_pubkey}'"
345 ),
346 }));
347 }
348 auth_settings.get_global_permission().ok_or_else(|| {
349 Error::Auth(AuthError::InvalidAuthConfiguration {
350 reason: "Global '*' permission not configured".to_string(),
351 })
352 })
353 }
354 SigKey::Direct(hint) => match (&hint.pubkey, &hint.name) {
355 (Some(pubkey), _) => {
356 // Verify the claimed pubkey matches the actual signing key
357 if *pubkey != actual_pubkey {
358 return Err(Error::Auth(AuthError::SigningKeyMismatch {
359 reason: format!(
360 "signing key derives pubkey '{actual_pubkey}' \
361 but identity claims '{pubkey}'"
362 ),
363 }));
364 }
365 let auth_key = auth_settings.get_key_by_pubkey(pubkey)?;
366 Ok(*auth_key.permissions())
367 }
368 (_, Some(name)) => {
369 let matches = auth_settings.find_keys_by_name(name);
370 if matches.is_empty() {
371 return Err(Error::Auth(AuthError::KeyNotFound {
372 key_name: name.clone(),
373 }));
374 }
375 // Find the named key whose pubkey matches our actual signing key
376 let actual_pubkey_str = actual_pubkey.to_string();
377 let (_, auth_key) = matches
378 .iter()
379 .find(|(pk, _)| *pk == actual_pubkey_str)
380 .ok_or_else(|| {
381 Error::Auth(AuthError::SigningKeyMismatch {
382 reason: format!(
383 "signing key derives pubkey '{actual_pubkey}' \
384 but no key named '{name}' has that pubkey"
385 ),
386 })
387 })?;
388 Ok(*auth_key.permissions())
389 }
390 _ => Err(Error::Auth(AuthError::InvalidAuthConfiguration {
391 reason: "DatabaseKey has empty identity hint".to_string(),
392 })),
393 },
394 SigKey::Delegation { .. } => {
395 // Resolve delegation path through AuthValidator
396 let instance = self.instance()?;
397 let mut validator = AuthValidator::new();
398 let resolved_auths = validator
399 .resolve_sig_key(key.identity(), &auth_settings, Some(&instance))
400 .await
401 .map_err(|e| {
402 Error::Auth(AuthError::InvalidAuthConfiguration {
403 reason: format!("Delegation resolution failed: {e}"),
404 })
405 })?;
406
407 // Find a resolved auth whose pubkey matches our signing key
408 resolved_auths
409 .into_iter()
410 .find(|ra| ra.public_key == actual_pubkey)
411 .map(|ra| ra.effective_permission)
412 .ok_or_else(|| {
413 Error::Auth(AuthError::SigningKeyMismatch {
414 reason: format!(
415 "signing key derives pubkey '{actual_pubkey}' \
416 but no resolved delegation key matches"
417 ),
418 })
419 })
420 }
421 }
422 }
423
424 /// Find all SigKeys that a public key can use to access a database.
425 ///
426 /// This static helper method loads a database's authentication settings and returns
427 /// all possible SigKeys that can be used with the given public key. This is useful for
428 /// discovering authentication options before opening a database.
429 ///
430 /// Returns all matching SigKeys including:
431 /// - Specific key names where the pubkey matches
432 /// - Global permission if available
433 /// - Single-hop delegation paths (pubkey found in a directly delegated tree)
434 ///
435 /// The results are **sorted by permission level, highest first**, making it easy to
436 /// select the most privileged access available.
437 ///
438 /// # Arguments
439 /// * `instance` - Instance handle for storage and coordination
440 /// * `root_id` - Root entry ID of the database to check
441 /// * `pubkey` - Public key string (e.g., "Ed25519:abc123...") to look up
442 ///
443 /// # Returns
444 /// A vector of (SigKey, Permission) tuples, sorted by permission (highest first).
445 /// Returns empty vector if no valid access methods are found.
446 ///
447 /// # Errors
448 /// Returns an error if:
449 /// - Database cannot be loaded
450 /// - Auth settings cannot be parsed
451 ///
452 /// # Example
453 /// ```rust,no_run
454 /// # use eidetica::*;
455 /// # use eidetica::database::DatabaseKey;
456 /// # use eidetica::backend::database::InMemory;
457 /// # use eidetica::auth::crypto::generate_keypair;
458 /// # use eidetica::auth::types::SigKey;
459 /// # #[tokio::main]
460 /// # async fn main() -> Result<()> {
461 /// # let instance = Instance::open(Box::new(InMemory::new())).await?;
462 /// # let (signing_key, pubkey) = generate_keypair();
463 /// # let root_id = "database_root_id".into();
464 /// // Find all SigKeys this pubkey can use (sorted highest permission first)
465 /// let sigkeys = Database::find_sigkeys(&instance, &root_id, &pubkey).await?;
466 ///
467 /// // Use the first available SigKey (highest permission)
468 /// if let Some((sigkey, _permission)) = sigkeys.first() {
469 /// let key = DatabaseKey::with_identity(signing_key, sigkey.clone());
470 /// let database = Database::open(instance, &root_id, key).await?;
471 /// }
472 /// # Ok(())
473 /// # }
474 /// ```
475 pub async fn find_sigkeys(
476 instance: &Instance,
477 root_id: &ID,
478 pubkey: &PublicKey,
479 ) -> Result<Vec<(SigKey, Permission)>> {
480 use crate::auth::{permission::clamp_permission, types::DelegationStep};
481
482 // Create temporary database to load settings (no key source needed for reading)
483 let temp_db = Self::open_unauthenticated(root_id.clone(), instance)?;
484
485 // Load auth settings
486 let settings_store = temp_db.get_settings().await?;
487 let auth_settings = settings_store.auth_snapshot().await?;
488
489 // Find direct SigKeys for this pubkey
490 let mut results = auth_settings.find_all_sigkeys_for_pubkey(pubkey);
491
492 // Scan single-hop delegation paths
493 // FIXME: deep nested delegations can't use this
494 if let Ok(delegated_trees) = auth_settings.get_all_delegated_trees() {
495 for (delegated_root_id, delegated_tree_ref) in &delegated_trees {
496 // Load the delegated tree's auth settings
497 let delegated_db =
498 match Self::open_unauthenticated(delegated_root_id.clone(), instance) {
499 Ok(db) => db,
500 Err(_) => continue,
501 };
502 let delegated_settings = match delegated_db.get_settings().await {
503 Ok(s) => s,
504 Err(_) => continue,
505 };
506 let delegated_auth = match delegated_settings.auth_snapshot().await {
507 Ok(a) => a,
508 Err(_) => continue,
509 };
510
511 // Check if pubkey exists in the delegated tree
512 let delegated_sigkeys = delegated_auth.find_all_sigkeys_for_pubkey(pubkey);
513 if delegated_sigkeys.is_empty() {
514 continue;
515 }
516
517 // Get current tips for the delegated tree
518 let tips = match instance.backend().get_tips(delegated_root_id).await {
519 Ok(t) => t,
520 Err(_) => continue,
521 };
522
523 // For each matching key in the delegated tree, construct a delegation SigKey
524 for (delegated_sk, delegated_perm) in delegated_sigkeys {
525 // Clamp the delegated permission through the bounds
526 let effective_perm =
527 clamp_permission(delegated_perm, &delegated_tree_ref.permission_bounds);
528
529 // Construct the delegation SigKey using the hint from the direct key
530 let delegation_sigkey = SigKey::Delegation {
531 path: vec![DelegationStep {
532 tree: delegated_root_id.to_string(),
533 tips: tips.clone(),
534 }],
535 hint: delegated_sk.hint().clone(),
536 };
537
538 results.push((delegation_sigkey, effective_perm));
539 }
540 }
541 }
542
543 // Sort by permission, highest first
544 results.sort_by(|a, b| b.1.cmp(&a.1));
545 Ok(results)
546 }
547
548 /// Get the auth identity for this database's configured key.
549 pub fn auth_identity(&self) -> Option<&SigKey> {
550 self.key.as_ref().map(|k| &k.identity)
551 }
552
553 /// Register an Instance-wide callback to be invoked when entries are written locally to this database.
554 ///
555 /// Local writes are those originating from transaction commits in the current Instance.
556 /// The callback receives the entry, database, and instance as parameters, providing
557 /// full context for any coordination or side effects needed.
558 ///
559 /// **Important:** This callback is registered at the Instance level and will fire for all local
560 /// writes to the database tree (identified by root ID), regardless of which Database handle
561 /// performed the write. Multiple Database handles pointing to the same root ID share the same
562 /// set of callbacks.
563 ///
564 /// # Arguments
565 /// * `callback` - Function to invoke on local writes to this database tree
566 ///
567 /// # Returns
568 /// A Result indicating success or failure
569 ///
570 /// # Example
571 /// ```rust,no_run
572 /// # use eidetica::*;
573 /// # use eidetica::crdt::Doc;
574 /// # use eidetica::backend::database::InMemory;
575 /// # use eidetica::auth::crypto::PrivateKey;
576 /// # #[tokio::main]
577 /// # async fn main() -> Result<()> {
578 /// let instance = Instance::open(Box::new(InMemory::new())).await?;
579 /// # let settings = eidetica::crdt::Doc::new();
580 /// # let signing_key = PrivateKey::generate();
581 /// # let database = Database::create(&instance, signing_key, Doc::new()).await?;
582 ///
583 /// database.on_local_write(|entry, db, _instance| {
584 /// let entry_id = entry.id().clone();
585 /// let db_id = db.root_id().clone();
586 /// Box::pin(async move {
587 /// println!("Entry {} written to database {}", entry_id, db_id);
588 /// Ok(())
589 /// })
590 /// })?;
591 /// # Ok(())
592 /// # }
593 /// ```
594 pub fn on_local_write<F, Fut>(&self, callback: F) -> Result<()>
595 where
596 F: for<'a> Fn(&'a Entry, &'a Database, &'a Instance) -> Fut
597 + Send
598 + std::marker::Sync
599 + 'static,
600 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
601 {
602 let instance = self.instance()?;
603 instance.register_write_callback(WriteSource::Local, self.root_id().clone(), callback)
604 }
605
606 /// Register an Instance-wide callback to be invoked when entries are written remotely to this database.
607 ///
608 /// Remote writes are those originating from sync or replication from other nodes.
609 /// The callback receives the entry, database, and instance as parameters.
610 ///
611 /// **Important:** This callback is registered at the Instance level and will fire for all remote
612 /// writes to the database tree (identified by root ID), regardless of which Database handle
613 /// registered the callback. Multiple Database handles pointing to the same root ID share the same
614 /// set of callbacks.
615 ///
616 /// # Arguments
617 /// * `callback` - Function to invoke on remote writes to this database tree
618 ///
619 /// # Returns
620 /// A Result indicating success or failure
621 ///
622 /// # Example
623 /// ```rust,no_run
624 /// # use eidetica::*;
625 /// # use eidetica::crdt::Doc;
626 /// # use eidetica::backend::database::InMemory;
627 /// # use eidetica::auth::crypto::PrivateKey;
628 /// # #[tokio::main]
629 /// # async fn main() -> Result<()> {
630 /// let instance = Instance::open(Box::new(InMemory::new())).await?;
631 /// # let settings = eidetica::crdt::Doc::new();
632 /// # let signing_key = PrivateKey::generate();
633 /// # let database = Database::create(&instance, signing_key, Doc::new()).await?;
634 ///
635 /// database.on_remote_write(|entry, db, _instance| {
636 /// let entry_id = entry.id().clone();
637 /// let db_id = db.root_id().clone();
638 /// Box::pin(async move {
639 /// println!("Remote entry {} synced to database {}", entry_id, db_id);
640 /// Ok(())
641 /// })
642 /// })?;
643 /// # Ok(())
644 /// # }
645 /// ```
646 pub fn on_remote_write<F, Fut>(&self, callback: F) -> Result<()>
647 where
648 F: for<'a> Fn(&'a Entry, &'a Database, &'a Instance) -> Fut
649 + Send
650 + std::marker::Sync
651 + 'static,
652 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
653 {
654 let instance = self.instance()?;
655 instance.register_write_callback(WriteSource::Remote, self.root_id().clone(), callback)
656 }
657
658 /// Get the ID of the root entry
659 pub fn root_id(&self) -> &ID {
660 &self.root
661 }
662
663 /// Upgrade the weak instance reference to a strong reference.
664 ///
665 /// # Returns
666 /// A `Result` containing the Instance or an error if the Instance has been dropped.
667 pub(crate) fn instance(&self) -> Result<Instance> {
668 self.instance
669 .upgrade()
670 .ok_or_else(|| Error::Instance(InstanceError::InstanceDropped))
671 }
672
673 /// Get a reference to the backend
674 pub fn backend(&self) -> Result<Backend> {
675 Ok(self.instance()?.backend().clone())
676 }
677
678 /// Retrieve the root entry from the backend
679 pub async fn get_root(&self) -> Result<Entry> {
680 let instance = self.instance()?;
681 instance.get(&self.root).await
682 }
683
684 /// Get a read-only settings store for the database.
685 ///
686 /// Returns a SettingsStore that provides access to the database's settings.
687 /// Since this creates an internal transaction that is never committed, any
688 /// modifications made through the returned store will not persist.
689 ///
690 /// For making persistent changes to settings, create a transaction and use
691 /// `Transaction::get_settings()` instead.
692 ///
693 /// # Returns
694 /// A `Result` containing the `SettingsStore` for settings or an error.
695 ///
696 /// # Example
697 /// ```rust,no_run
698 /// # use eidetica::Database;
699 /// # async fn example(database: Database) -> eidetica::Result<()> {
700 /// // Read-only access
701 /// let settings = database.get_settings().await?;
702 /// let name = settings.get_name().await?;
703 ///
704 /// // For modifications, use a transaction:
705 /// let txn = database.new_transaction().await?;
706 /// let settings = txn.get_settings()?;
707 /// settings.set_name("new_name").await?;
708 /// txn.commit().await?;
709 /// # Ok(())
710 /// # }
711 /// ```
712 pub async fn get_settings(&self) -> Result<SettingsStore> {
713 let txn = self.new_transaction().await?;
714 txn.get_settings()
715 }
716
717 /// Get the name of the database from its settings store
718 pub async fn get_name(&self) -> Result<String> {
719 let settings = self.get_settings().await?;
720 settings.get_name().await
721 }
722
723 /// Create a new atomic transaction on this database
724 ///
725 /// This creates a new atomic transaction containing a new Entry.
726 /// The atomic transaction will be initialized with the current state of the database.
727 /// If a default authentication key is set, the transaction will use it for signing.
728 ///
729 /// # Returns
730 /// A `Result<Transaction>` containing the new atomic transaction
731 pub async fn new_transaction(&self) -> Result<Transaction> {
732 let tips = self.get_tips().await?;
733 self.new_transaction_with_tips(&tips).await
734 }
735
736 /// Create a new atomic transaction on this database with specific parent tips
737 ///
738 /// This creates a new atomic transaction that will have the specified entries as parents
739 /// instead of using the current database tips. This allows creating complex DAG structures
740 /// like diamond patterns for testing and advanced use cases.
741 ///
742 /// # Arguments
743 /// * `tips` - The specific parent tips to use for this transaction
744 ///
745 /// # Returns
746 /// A `Result<Transaction>` containing the new atomic transaction
747 pub async fn new_transaction_with_tips(&self, tips: impl AsRef<[ID]>) -> Result<Transaction> {
748 let mut txn = Transaction::new_with_tips(self, tips.as_ref()).await?;
749
750 // Set provided signing key from DatabaseKey
751 if let Some(key) = &self.key {
752 txn.set_provided_key(*key.signing_key.clone(), key.identity.clone());
753 }
754
755 Ok(txn)
756 }
757
758 /// Insert an entry into the database without modifying it.
759 /// This is primarily for testing purposes or when you need full control over the entry.
760 /// Note: This method assumes the entry is already properly signed and verified.
761 pub async fn insert_raw(&self, entry: Entry) -> Result<ID> {
762 let instance = self.instance()?;
763 let id = entry.id();
764
765 instance.put(VerificationStatus::Verified, entry).await?;
766
767 Ok(id)
768 }
769
770 /// Get a Store type that will handle accesses to the Store
771 /// This will return a Store initialized to point at the current state of the database.
772 ///
773 /// The returned store should NOT be used to modify the database, as it intentionally does not
774 /// expose the Transaction. Since the Transaction is never committed, it does not have any
775 /// effect on the database.
776 pub async fn get_store_viewer<T>(&self, name: impl Into<String>) -> Result<T>
777 where
778 T: Store,
779 {
780 let txn = self.new_transaction().await?;
781 T::new(&txn, name.into()).await
782 }
783
784 /// Get the current tips (leaf entries) of the main database branch.
785 ///
786 /// Tips represent the latest entries in the database's main history, forming the heads of the DAG.
787 ///
788 /// # Returns
789 /// A `Result` containing a vector of `ID`s for the tip entries or an error.
790 pub async fn get_tips(&self) -> Result<Vec<ID>> {
791 let instance = self.instance()?;
792 instance.get_tips(&self.root).await
793 }
794
795 /// Get the full `Entry` objects for the current tips of the main database branch.
796 ///
797 /// # Returns
798 /// A `Result` containing a vector of the tip `Entry` objects or an error.
799 pub async fn get_tip_entries(&self) -> Result<Vec<Entry>> {
800 let instance = self.instance()?;
801 let tips = instance.get_tips(&self.root).await?;
802 let mut entries = Vec::new();
803 for id in &tips {
804 entries.push(instance.get(id).await?);
805 }
806 Ok(entries)
807 }
808
809 /// Get a single entry by ID from this database.
810 ///
811 /// This is the primary method for retrieving entries after commit operations.
812 /// It provides safe, high-level access to entry data without exposing backend details.
813 ///
814 /// The method verifies that the entry belongs to this database by checking its root ID.
815 /// If the entry exists but belongs to a different database, an error is returned.
816 ///
817 /// # Arguments
818 /// * `entry_id` - The ID of the entry to retrieve (accepts anything that converts to ID/String)
819 ///
820 /// # Returns
821 /// A `Result` containing the `Entry` or an error if not found or not part of this database
822 ///
823 /// # Example
824 /// ```rust,no_run
825 /// # use eidetica::*;
826 /// # use eidetica::Instance;
827 /// # use eidetica::backend::database::InMemory;
828 /// # use eidetica::crdt::Doc;
829 /// # #[tokio::main]
830 /// # async fn main() -> Result<()> {
831 /// # let backend = Box::new(InMemory::new());
832 /// # let instance = Instance::open(backend).await?;
833 /// # instance.create_user("test", None).await?;
834 /// # let mut user = instance.login_user("test", None).await?;
835 /// # let key_id = user.add_private_key(None).await?;
836 /// # let tree = user.create_database(Doc::new(), &key_id).await?;
837 /// # let txn = tree.new_transaction().await?;
838 /// let entry_id = txn.commit().await?;
839 /// let entry = tree.get_entry(&entry_id).await?; // Using &String
840 /// let entry = tree.get_entry("some_entry_id").await?; // Using &str
841 /// let entry = tree.get_entry(entry_id.clone()).await?; // Using String
842 /// println!("Entry signature: {:?}", entry.sig);
843 /// # Ok(())
844 /// # }
845 /// ```
846 pub async fn get_entry<I: Into<ID>>(&self, entry_id: I) -> Result<Entry> {
847 let instance = self.instance()?;
848 let id = entry_id.into();
849 let entry = instance.get(&id).await?;
850
851 // Check if the entry belongs to this database
852 if !entry.in_tree(&self.root) {
853 return Err(InstanceError::EntryNotInDatabase {
854 entry_id: id,
855 database_id: self.root.clone(),
856 }
857 .into());
858 }
859
860 Ok(entry)
861 }
862
863 /// Get multiple entries by ID efficiently.
864 ///
865 /// This method retrieves multiple entries more efficiently than multiple `get_entry()` calls
866 /// by minimizing conversion overhead and pre-allocating the result vector.
867 ///
868 /// The method verifies that all entries belong to this database by checking their root IDs.
869 /// If any entry exists but belongs to a different database, an error is returned.
870 ///
871 /// # Parameters
872 /// * `entry_ids` - An iterable of entry IDs to retrieve. Accepts any string or ID types
873 /// that can be converted to `ID` (`&str`, `String`, `&ID`, etc.)
874 ///
875 /// # Returns
876 /// A `Result` containing a vector of `Entry` objects or an error if any entry is not found or not part of this database
877 ///
878 /// # Example
879 /// ```rust,no_run
880 /// # use eidetica::*;
881 /// # use eidetica::Instance;
882 /// # use eidetica::backend::database::InMemory;
883 /// # use eidetica::crdt::Doc;
884 /// # #[tokio::main]
885 /// # async fn main() -> Result<()> {
886 /// # let backend = Box::new(InMemory::new());
887 /// # let instance = Instance::open(backend).await?;
888 /// # instance.create_user("test", None).await?;
889 /// # let mut user = instance.login_user("test", None).await?;
890 /// # let key_id = user.add_private_key(None).await?;
891 /// # let tree = user.create_database(Doc::new(), &key_id).await?;
892 /// let entry_ids = vec!["id1", "id2", "id3"];
893 /// let entries = tree.get_entries(entry_ids).await?;
894 /// # Ok(())
895 /// # }
896 /// ```
897 pub async fn get_entries<I, T>(&self, entry_ids: I) -> Result<Vec<Entry>>
898 where
899 I: IntoIterator<Item = T>,
900 T: Into<ID>,
901 {
902 // Collect IDs first to minimize conversions and avoid repeat work in iterator chain
903 let ids: Vec<ID> = entry_ids.into_iter().map(Into::into).collect();
904 let instance = self.instance()?;
905 let mut entries = Vec::with_capacity(ids.len());
906
907 for id in ids {
908 let entry = instance.get(&id).await?;
909
910 // Check if the entry belongs to this database
911 if !entry.in_tree(&self.root) {
912 return Err(InstanceError::EntryNotInDatabase {
913 entry_id: id,
914 database_id: self.root.clone(),
915 }
916 .into());
917 }
918
919 entries.push(entry);
920 }
921
922 Ok(entries)
923 }
924
925 // === AUTHENTICATION HELPERS ===
926
927 /// Verify an entry's signature and authentication against the database's configuration that was valid at the time of entry creation.
928 ///
929 /// This method validates that:
930 /// 1. The entry belongs to this database
931 /// 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
932 /// 3. The signature is cryptographically valid
933 ///
934 /// The method uses the entry's metadata to determine which authentication settings were active when the entry was signed,
935 /// ensuring that entries remain valid even if keys are later revoked or settings change.
936 ///
937 /// # Arguments
938 /// * `entry_id` - The ID of the entry to verify (accepts anything that converts to ID/String)
939 ///
940 /// # Returns
941 /// A `Result` containing `true` if the entry is valid and properly authenticated, `false` if authentication fails
942 ///
943 /// # Errors
944 /// Returns an error if:
945 /// - The entry is not found
946 /// - The entry does not belong to this database
947 /// - The entry's metadata cannot be parsed
948 /// - The historical authentication settings cannot be retrieved
949 pub async fn verify_entry_signature<I: Into<ID>>(&self, entry_id: I) -> Result<bool> {
950 let entry = self.get_entry(entry_id).await?;
951
952 // Get the authentication settings that were valid at the time this entry was created
953 let historical_settings = self.get_historical_settings_for_entry(&entry).await?;
954
955 // Use the authentication validator with historical settings
956 let instance = self.instance()?;
957 let mut validator = AuthValidator::new();
958 validator
959 .validate_entry(&entry, &historical_settings, Some(&instance))
960 .await
961 }
962
963 /// Get the permission level for this database's configured signing key.
964 ///
965 /// Returns the effective permission for the key that was configured when opening
966 /// or creating this database. This uses the already-resolved identity stored in
967 /// the database's `DatabaseKey`, ensuring consistency with `Database::open`.
968 ///
969 /// # Returns
970 /// The effective Permission for the configured signing key.
971 ///
972 /// # Errors
973 /// Returns an error if:
974 /// - No signing key is configured (database opened without authentication)
975 /// - The database settings cannot be retrieved
976 /// - The key is no longer valid in the current auth settings
977 ///
978 /// # Example
979 /// ```rust,no_run
980 /// # use eidetica::*;
981 /// # use eidetica::crdt::Doc;
982 /// # use eidetica::backend::database::InMemory;
983 /// # use eidetica::auth::crypto::generate_keypair;
984 /// # #[tokio::main]
985 /// # async fn main() -> Result<()> {
986 /// # let instance = Instance::open(Box::new(InMemory::new())).await?;
987 /// # let (signing_key, _public_key) = generate_keypair();
988 /// # let database = Database::create(&instance, signing_key, Doc::new()).await?;
989 /// // Check if the current key has Admin permission
990 /// let permission = database.current_permission().await?;
991 /// if permission.can_admin() {
992 /// println!("Current key has Admin permission!");
993 /// }
994 /// # Ok(())
995 /// # }
996 /// ```
997 pub async fn current_permission(&self) -> Result<Permission> {
998 let key = self
999 .key
1000 .as_ref()
1001 .ok_or(AuthError::InvalidAuthConfiguration {
1002 reason: "No signing key configured for this database".to_string(),
1003 })?;
1004 self.validate_key(key).await
1005 }
1006
1007 /// Get the authentication settings that were valid when a specific entry was created.
1008 ///
1009 /// This method examines the entry's metadata to find the settings tips that were active
1010 /// at the time of entry creation, then reconstructs the historical settings state.
1011 ///
1012 /// # Arguments
1013 /// * `entry` - The entry to get historical settings for
1014 ///
1015 /// # Returns
1016 /// A `Result` containing the historical authentication settings
1017 async fn get_historical_settings_for_entry(&self, _entry: &Entry) -> Result<AuthSettings> {
1018 // TODO: Implement full historical settings reconstruction from entry metadata
1019 // For now, use current settings for simplicity and backward compatibility
1020 //
1021 // The complete implementation would:
1022 // 1. Parse entry metadata to get settings tips active at entry creation time
1023 // 2. Reconstruct the CRDT state from those historical tips
1024 // 3. Validate against that historical state
1025 //
1026 // This ensures entries remain valid even if keys are later revoked,
1027 // but requires more complex CRDT state reconstruction logic.
1028
1029 let settings = self.get_settings().await?;
1030 settings.auth_snapshot().await
1031 }
1032
1033 // === DATABASE QUERIES ===
1034
1035 /// Get all entries in this database.
1036 ///
1037 /// ⚠️ **Warning**: This method loads all entries into memory. Use with caution on large databases.
1038 /// Consider using `get_tips()` or `get_tip_entries()` for more efficient access patterns.
1039 ///
1040 /// # Returns
1041 /// A `Result` containing a vector of all `Entry` objects in the database
1042 pub async fn get_all_entries(&self) -> Result<Vec<Entry>> {
1043 let instance = self.instance()?;
1044 instance.backend().get_tree(&self.root).await
1045 }
1046}