Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/user/session/
mod.rs

1//! User session management
2//!
3//! Represents an authenticated user session with decrypted keys.
4//!
5//! # API Overview
6//!
7//! The User API is organized into three areas for managing Databases:
8//!
9//! ## Database Lifecycle
10//!
11//! - **`create_database()`** - Create a new database
12//! - **`open_database()`** - Open an existing database
13//! - **`find_database()`** - Search for databases by name
14//!
15//! ## Tracked Databases
16//!
17//! Manage your personal list of tracked databases:
18//!
19//! - **`databases()`** - List all tracked databases
20//! - **`database()`** - Get a specific tracked database
21//! - **`track_database()`** - Add or update a tracked database (upsert)
22//! - **`untrack_database()`** - Remove a database from your tracked list
23//!
24//! ## Key-Database Mappings
25//!
26//! Control which keys access which databases:
27//!
28//! - **`map_key()`** - Map a key to a SigKey identifier for a database
29//! - **`key_mapping()`** - Get the SigKey mapping for a key-database pair
30//! - **`find_key()`** - Find which key can access a database
31//!
32//! This explicit approach ensures predictable behavior and avoids ambiguity about which
33//! keys have access to which databases.
34
35use handle_trait::Handle;
36use std::collections::HashMap;
37
38use super::{UserKeyManager, types::UserInfo};
39use crate::{
40    Database, Error, Instance, Result, Transaction,
41    auth::{Permission, SigKey, crypto::PublicKey},
42    crdt::Doc,
43    database::DatabaseKey,
44    entry::ID,
45    instance::{InstanceError, backend::Backend},
46    store::Table,
47    sync::{BootstrapRequest, DatabaseTicket, Sync},
48    user::{SyncSettings, TrackedDatabase, UserError},
49};
50
51#[cfg(test)]
52mod tests;
53
54/// User session object, returned after successful login
55///
56/// Represents an authenticated user with decrypted private keys loaded in memory.
57/// The User struct provides access to key management, tracked databases, and
58/// bootstrap approval operations.
59pub struct User {
60    /// Stable internal user UUID (Table primary key)
61    user_uuid: String,
62
63    /// Username (login identifier)
64    username: String,
65
66    /// User's private database (contains encrypted keys and tracked databases)
67    user_database: Database,
68
69    /// Instance reference for database operations
70    instance: Instance,
71
72    /// Decrypted user keys (in memory only during session)
73    key_manager: UserKeyManager,
74
75    /// User info (cached from _users database)
76    user_info: UserInfo,
77}
78
79impl std::fmt::Debug for User {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        f.debug_struct("User")
82            .field("user_uuid", &self.user_uuid)
83            .field("username", &self.username)
84            .field("user_database", &self.user_database)
85            .field("instance", &self.instance)
86            .field("key_manager", &"<KeyManager [sensitive]>")
87            .field("user_info", &self.user_info)
88            .finish()
89    }
90}
91
92impl User {
93    /// Create a new User session
94    ///
95    /// This is an internal constructor used after successful login.
96    /// Use `Instance::login_user()` to create a User session.
97    ///
98    /// # Arguments
99    /// * `user_uuid` - Internal UUID (Table primary key)
100    /// * `user_info` - User information from _users database
101    /// * `user_database` - The user's private database
102    /// * `instance` - Instance reference
103    /// * `key_manager` - Initialized key manager with decrypted keys
104    #[allow(dead_code)]
105    pub(crate) fn new(
106        user_uuid: String,
107        user_info: UserInfo,
108        user_database: Database,
109        instance: Instance,
110        key_manager: UserKeyManager,
111    ) -> Self {
112        Self {
113            user_uuid,
114            username: user_info.username.clone(),
115            user_database,
116            instance,
117            key_manager,
118            user_info,
119        }
120    }
121
122    // === Basic Session Methods ===
123
124    /// Get the internal user UUID (stable identifier)
125    pub fn user_uuid(&self) -> &str {
126        &self.user_uuid
127    }
128
129    /// Get the username (login identifier)
130    pub fn username(&self) -> &str {
131        &self.username
132    }
133
134    /// Get a reference to the user's database
135    pub fn user_database(&self) -> &Database {
136        &self.user_database
137    }
138
139    /// Get a reference to the backend
140    pub fn backend(&self) -> &Backend {
141        self.instance.backend()
142    }
143
144    /// Get a reference to the user info
145    pub fn user_info(&self) -> &UserInfo {
146        &self.user_info
147    }
148
149    /// Logout (consumes self and clears decrypted keys from memory)
150    ///
151    /// After logout, all decrypted keys are zeroized and the session is ended.
152    /// Keys are automatically cleared when the User is dropped.
153    pub fn logout(self) -> Result<()> {
154        // Consume self, all keys are stored in other Types that zeroize themselves on drop
155        Ok(())
156    }
157
158    // === Key Manager Access (Internal) ===
159
160    /// Get a reference to the key manager (for internal use)
161    #[allow(dead_code)]
162    pub(crate) fn key_manager(&self) -> &UserKeyManager {
163        &self.key_manager
164    }
165
166    /// Get a mutable reference to the key manager (for internal use)
167    #[allow(dead_code)]
168    pub(crate) fn key_manager_mut(&mut self) -> &mut UserKeyManager {
169        &mut self.key_manager
170    }
171
172    // === Database Operations (User Context) ===
173
174    /// Create a new database with explicit key selection.
175    ///
176    /// This method requires you to specify which key should be used to create and manage
177    /// the database, providing explicit control over key-database relationships.
178    ///
179    /// # Arguments
180    /// * `settings` - Initial database settings (metadata, name, etc.)
181    /// * `key_id` - The ID of the key to use for this database (public key string)
182    ///
183    /// # Returns
184    /// The created Database
185    ///
186    /// # Errors
187    /// - Returns an error if the specified key_id doesn't exist
188    /// - Returns an error if the key cannot be retrieved
189    ///
190    /// # Example
191    /// ```rust,ignore
192    /// // Get available keys
193    /// let keys = user.list_keys()?;
194    /// let key_id = &keys[1]; // Use the second key
195    ///
196    /// // Create database with explicit key selection
197    /// let mut settings = Doc::new();
198    /// settings.set("name", "My Database");
199    /// let database = user.new_database(settings, key_id)?;
200    /// ```
201    pub async fn create_database(&mut self, settings: Doc, key_id: &PublicKey) -> Result<Database> {
202        use crate::user::types::{SyncSettings, UserKey};
203
204        // Get the signing key from UserKeyManager
205        let signing_key = self
206            .key_manager
207            .get_signing_key(key_id)
208            .ok_or_else(|| UserError::KeyNotFound {
209                key_id: key_id.to_string(),
210            })?
211            .clone();
212
213        // Create the database with the provided key directly
214        let database = Database::create(&self.instance, signing_key, settings).await?;
215
216        // Store the mapping in UserKey and track the database
217        let tx = self.user_database.new_transaction().await?;
218        let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
219
220        // Find the key metadata in the database
221        let (uuid_primary_key, mut metadata) = keys_table
222            .search(|uk| &uk.key_id == key_id)
223            .await?
224            .into_iter()
225            .next()
226            .ok_or_else(|| UserError::KeyNotFound {
227                key_id: key_id.to_string(),
228            })?;
229
230        // Add the database sigkey mapping (None = default pubkey identity)
231        metadata
232            .database_sigkeys
233            .insert(database.root_id().clone(), None);
234
235        // Update the key in user database using the UUID primary key
236        keys_table.set(&uuid_primary_key, metadata.clone()).await?;
237
238        // Also track the database in the databases table
239        let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
240        let tracked = TrackedDatabase {
241            database_id: database.root_id().clone(),
242            key_id: key_id.clone(),
243            sync_settings: SyncSettings::disabled(),
244        };
245        databases_table.set(database.root_id(), tracked).await?;
246
247        tx.commit().await?;
248
249        // Update the in-memory key manager with the updated metadata
250        self.key_manager.add_key(metadata)?;
251
252        Ok(database)
253    }
254
255    /// Open an existing database by its root ID using this user's keys.
256    ///
257    /// This method automatically:
258    /// 1. Finds an appropriate key that has access to the database
259    /// 2. Retrieves the decrypted SigningKey from the UserKeyManager
260    /// 3. Gets the SigKey mapping for this database
261    /// 4. Creates a Database instance configured with the user's key
262    ///
263    /// The returned Database will use the user's provided key for all operations,
264    /// without requiring backend key lookups.
265    ///
266    /// # Arguments
267    /// * `root_id` - The root entry ID of the database
268    ///
269    /// # Returns
270    /// The opened Database configured to use this user's keys
271    ///
272    /// # Errors
273    /// - Returns an error if no key is found for the database
274    /// - Returns an error if no SigKey mapping exists
275    /// - Returns an error if the key is not in the UserKeyManager
276    pub async fn open_database(&self, root_id: &ID) -> Result<Database> {
277        // Validate the root exists
278        self.instance.backend().get(root_id).await?;
279
280        // Find an appropriate key for this database
281        let key_id =
282            self.find_key(root_id)?
283                .ok_or_else(|| super::errors::UserError::NoKeyForDatabase {
284                    database_id: root_id.clone(),
285                })?;
286
287        // Get the SigningKey from UserKeyManager
288        let signing_key = self.key_manager.get_signing_key(&key_id).ok_or_else(|| {
289            super::errors::UserError::KeyNotFound {
290                key_id: key_id.to_string(),
291            }
292        })?;
293
294        // Get the SigKey mapping for this database
295        let sigkey = self.key_mapping(&key_id, root_id)?.ok_or_else(|| {
296            super::errors::UserError::NoSigKeyMapping {
297                key_id: key_id.to_string(),
298                database_id: root_id.clone(),
299            }
300        })?;
301
302        // Create Database with user-provided key using resolved SigKey identity
303        let key = DatabaseKey::with_identity(signing_key.clone(), sigkey);
304        Database::open(self.instance.handle(), root_id, key).await
305    }
306
307    /// Find databases by name among the user's tracked databases.
308    ///
309    /// Searches only the databases this user has tracked for those matching the given name.
310    ///
311    /// # Arguments
312    /// * `name` - Database name to search for
313    ///
314    /// # Returns
315    /// Vector of matching databases from the user's tracked list
316    pub async fn find_database(&self, name: impl AsRef<str>) -> Result<Vec<Database>> {
317        let name = name.as_ref();
318        let tracked = self.databases().await?;
319        let mut matching = Vec::new();
320
321        for tracked_db in tracked {
322            if let Ok(database) = self.open_database(&tracked_db.database_id).await
323                && let Ok(db_name) = database.get_name().await
324                && db_name == name
325            {
326                matching.push(database);
327            }
328        }
329
330        if matching.is_empty() {
331            Err(UserError::DatabaseNotTracked {
332                database_id: format!("name:{name}"),
333            }
334            .into())
335        } else {
336            Ok(matching)
337        }
338    }
339
340    /// Find which key can access a database.
341    ///
342    /// Searches this user's keys to find one that can access the specified database.
343    /// Considers the SigKey mappings stored in user key metadata.
344    ///
345    /// Returns the key_id of a suitable key, preferring keys with mappings for this database.
346    ///
347    /// # Arguments
348    /// * `database_id` - The ID of the database
349    ///
350    /// # Returns
351    /// Some(key_id) if a suitable key is found, None if no keys can access this database
352    pub fn find_key(&self, database_id: &ID) -> Result<Option<PublicKey>> {
353        // Iterate through all keys and find ones with SigKey mappings for this database
354        for key_id in self.key_manager.list_key_ids() {
355            if let Some(metadata) = self.key_manager.get_key_metadata(&key_id)
356                && metadata.database_sigkeys.contains_key(database_id)
357            {
358                return Ok(Some(key_id));
359            }
360        }
361
362        // No key found with mapping for this database
363        Ok(None)
364    }
365
366    /// Get the resolved SigKey mapping for a key in a specific database.
367    ///
368    /// Users map their private keys to SigKey identifiers on a per-database basis.
369    /// This retrieves the resolved SigKey that a specific key uses in
370    /// a specific database's authentication settings.
371    ///
372    /// Internally, `None` in the stored mapping means "default pubkey identity",
373    /// which this method resolves to the concrete `SigKey::from_pubkey(...)` value.
374    ///
375    /// # Arguments
376    /// * `key_id` - The user's key identifier
377    /// * `database_id` - The database ID
378    ///
379    /// # Returns
380    /// `Ok(Some(sigkey))` if a mapping exists (resolved to concrete SigKey),
381    /// `Ok(None)` if no mapping is configured for this database
382    ///
383    /// # Errors
384    /// Returns an error if the key_id doesn't exist in the UserKeyManager
385    pub fn key_mapping(&self, key_id: &PublicKey, database_id: &ID) -> Result<Option<SigKey>> {
386        let metadata = self.key_manager.get_key_metadata(key_id).ok_or_else(|| {
387            super::errors::UserError::KeyNotFound {
388                key_id: key_id.to_string(),
389            }
390        })?;
391
392        match metadata.database_sigkeys.get(database_id) {
393            None => Ok(None), // no mapping exists
394            Some(None) => {
395                // Default: pubkey identity derived directly from key_id
396                Ok(Some(SigKey::from_pubkey(key_id)))
397            }
398            Some(Some(sigkey)) => Ok(Some(sigkey.clone())),
399        }
400    }
401
402    /// Map a key to a SigKey identity for a specific database.
403    ///
404    /// Registers that this user's key should be used with a specific SigKey identity
405    /// when interacting with a database. This is typically used when a user has been
406    /// granted access to a database and needs to configure their local key to work with it.
407    ///
408    /// If the provided SigKey matches the default pubkey identity for this key,
409    /// it is normalized to `None` internally (compact storage for the common case).
410    ///
411    /// # Multi-Key Support
412    ///
413    /// **Note**: A database may have mappings to multiple keys. This is useful for
414    /// multi-device scenarios where the same user wants to access a database from
415    /// different devices, each with their own key.
416    ///
417    /// # Arguments
418    /// * `key_id` - The user's key identifier (public key)
419    /// * `database_id` - The database ID
420    /// * `sigkey` - The SigKey identity to use for this database
421    ///
422    /// # Errors
423    /// Returns an error if the key_id doesn't exist in the user database
424    pub async fn map_key(
425        &mut self,
426        key_id: &PublicKey,
427        database_id: &ID,
428        sigkey: SigKey,
429    ) -> Result<()> {
430        let tx = self.user_database.new_transaction().await?;
431        self.map_key_in_txn(&tx, key_id, database_id, sigkey)
432            .await?;
433        tx.commit().await?;
434        Ok(())
435    }
436
437    /// Internal helper: Add a SigKey mapping within an existing transaction
438    ///
439    /// This is used internally by methods that manage their own transactions.
440    /// For external use, call `map_key()` instead.
441    ///
442    /// Normalizes the stored value: if the sigkey matches the default pubkey
443    /// identity for this key, stores `None` instead of `Some(sigkey)`.
444    async fn map_key_in_txn(
445        &mut self,
446        tx: &Transaction,
447        key_id: &PublicKey,
448        database_id: &ID,
449        sigkey: SigKey,
450    ) -> Result<()> {
451        use crate::store::Table;
452        use crate::user::types::UserKey;
453
454        let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
455
456        // Find the key metadata in the database
457        let (uuid_primary_key, mut metadata) = keys_table
458            .search(|uk| &uk.key_id == key_id)
459            .await?
460            .into_iter()
461            .next()
462            .ok_or_else(|| super::errors::UserError::KeyNotFound {
463                key_id: key_id.to_string(),
464            })?;
465
466        // Normalize: if the sigkey matches the default pubkey identity, store None
467        let default_sigkey = SigKey::from_pubkey(key_id);
468        let stored = if sigkey == default_sigkey {
469            None
470        } else {
471            Some(sigkey)
472        };
473
474        // Add the database sigkey mapping
475        metadata
476            .database_sigkeys
477            .insert(database_id.clone(), stored);
478
479        // Update the key in user database using the UUID primary key
480        keys_table.set(&uuid_primary_key, metadata.clone()).await?;
481
482        // Update the in-memory key manager with the updated metadata
483        self.key_manager.add_key(metadata)?;
484
485        Ok(())
486    }
487
488    /// Internal helper: Validate key and set up SigKey mapping within an existing transaction
489    ///
490    /// This validates that a key exists and has access to a database, discovers the appropriate
491    /// SigKey, and creates the mapping. Used by track_database (which has upsert behavior).
492    async fn validate_and_map_key_in_txn(
493        &mut self,
494        tx: &Transaction,
495        database_id: &ID,
496        key_id: &PublicKey,
497    ) -> Result<()> {
498        // Verify the key exists
499        if self.key_manager.get_signing_key(key_id).is_none() {
500            return Err(UserError::KeyNotFound {
501                key_id: key_id.to_string(),
502            }
503            .into());
504        }
505
506        // Discover available SigKeys for this public key
507        let available_sigkeys = Database::find_sigkeys(&self.instance, database_id, key_id).await?;
508
509        if available_sigkeys.is_empty() {
510            return Err(UserError::NoSigKeyFound {
511                key_id: key_id.to_string(),
512                database_id: database_id.clone(),
513            }
514            .into());
515        }
516
517        // Select the first SigKey (highest permission, since find_sigkeys returns sorted list)
518        let (sigkey, _permission) = &available_sigkeys[0];
519
520        // Store the discovered SigKey directly (map_key_in_txn normalizes to None if default)
521        self.map_key_in_txn(tx, key_id, database_id, sigkey.clone())
522            .await?;
523
524        Ok(())
525    }
526
527    // === Key Management (User Context) ===
528
529    /// Add a new private key to this user's keyring.
530    ///
531    /// Generates a new Ed25519 keypair, encrypts it (for password-protected users)
532    /// or stores it unencrypted (for passwordless users), and adds it to the user's
533    /// key database.
534    ///
535    /// # Arguments
536    /// * `display_name` - Optional display name for the key
537    ///
538    /// # Returns
539    /// The key ID (public key string)
540    pub async fn add_private_key(&mut self, display_name: Option<&str>) -> Result<PublicKey> {
541        use crate::auth::crypto::generate_keypair;
542        use crate::store::Table;
543        use crate::user::types::{KeyStorage, UserKey};
544
545        // Generate new keypair
546        let (private_key, public_key) = generate_keypair();
547
548        // Get current timestamp using the instance's clock
549        let timestamp = self.instance.clock().now_secs();
550
551        // Prepare UserKey based on encryption type
552        let user_key = if let Some(encryption_key) = self.key_manager.encryption_key() {
553            // Password-protected user: encrypt the key
554            use crate::user::crypto::encrypt_private_key;
555            let (ciphertext, nonce) = encrypt_private_key(&private_key, encryption_key)?;
556
557            UserKey {
558                key_id: public_key.clone(),
559                storage: KeyStorage::Encrypted {
560                    algorithm: "aes-256-gcm".to_string(),
561                    ciphertext,
562                    nonce,
563                },
564                display_name: display_name.map(|s| s.to_string()),
565                created_at: timestamp,
566                last_used: None,
567                is_default: false, // New keys are not default
568                database_sigkeys: HashMap::new(),
569            }
570        } else {
571            // Passwordless user: store unencrypted
572            UserKey {
573                key_id: public_key.clone(),
574                storage: KeyStorage::Unencrypted { key: private_key },
575                display_name: display_name.map(|s| s.to_string()),
576                created_at: timestamp,
577                last_used: None,
578                is_default: false, // New keys are not default
579                database_sigkeys: HashMap::new(),
580            }
581        };
582
583        // Store in user database
584        let tx = self.user_database.new_transaction().await?;
585        let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
586        keys_table.insert(user_key.clone()).await?;
587        tx.commit().await?;
588
589        // Add to in-memory key manager
590        self.key_manager.add_key(user_key)?;
591
592        Ok(public_key)
593    }
594
595    /// List all key IDs owned by this user.
596    ///
597    /// Keys are returned sorted by creation timestamp (oldest first), making the
598    /// first key in the list the "default" key created when the user was set up.
599    ///
600    /// # Returns
601    /// Vector of PublicKeys sorted by creation time
602    pub fn list_keys(&self) -> Result<Vec<PublicKey>> {
603        Ok(self.key_manager.list_key_ids())
604    }
605
606    /// Get the default key.
607    ///
608    /// Returns the key marked as is_default=true, or falls back to the oldest key
609    /// by creation timestamp if no default is explicitly set.
610    ///
611    /// # Returns
612    /// The PublicKey of the default key
613    ///
614    /// # Errors
615    /// Returns an error if no keys exist
616    pub fn get_default_key(&self) -> Result<PublicKey> {
617        self.key_manager
618            .get_default_key_id()
619            .ok_or_else(|| Error::from(InstanceError::AuthenticationRequired))
620    }
621
622    /// Get a signing key by its ID.
623    ///
624    /// # Arguments
625    /// * `key_id` - The public key identifier
626    ///
627    /// # Returns
628    /// The PrivateKey if found
629    #[cfg(any(test, feature = "testing"))]
630    pub fn get_signing_key(&self, key_id: &PublicKey) -> Result<crate::auth::crypto::PrivateKey> {
631        self.key_manager
632            .get_signing_key(key_id)
633            .cloned()
634            .ok_or_else(|| {
635                UserError::KeyNotFound {
636                    key_id: key_id.to_string(),
637                }
638                .into()
639            })
640    }
641
642    // === Bootstrap Request Management (User Context) ===
643
644    /// Get all pending bootstrap requests from the sync system.
645    ///
646    /// This is a convenience method that requires the Instance's Sync to be initialized.
647    ///
648    /// # Arguments
649    /// * `sync` - Reference to the Instance's Sync object
650    ///
651    /// # Returns
652    /// A vector of (request_id, bootstrap_request) pairs for pending requests
653    pub async fn pending_bootstrap_requests(
654        &self,
655        sync: &Sync,
656    ) -> Result<Vec<(String, BootstrapRequest)>> {
657        sync.pending_bootstrap_requests().await
658    }
659
660    /// Approve a bootstrap request and add the requesting key to the target database.
661    ///
662    /// The approving key must have Admin permission on the target database.
663    ///
664    /// # Arguments
665    /// * `sync` - Mutable reference to the Instance's Sync object
666    /// * `request_id` - The unique identifier of the request to approve
667    /// * `approving_key_id` - The ID of this user's key to use for approval (must have Admin permission)
668    ///
669    /// # Returns
670    /// Result indicating success or failure of the approval operation
671    ///
672    /// # Errors
673    /// - Returns an error if the user doesn't own the specified approving key
674    /// - Returns an error if the approving key doesn't have Admin permission on the target database
675    /// - Returns an error if the request doesn't exist or isn't pending
676    /// - Returns an error if the key addition to the database fails
677    pub async fn approve_bootstrap_request(
678        &self,
679        sync: &Sync,
680        request_id: &str,
681        approving_key_id: &PublicKey,
682    ) -> Result<()> {
683        // Get the signing key from the key manager
684        let signing_key = self
685            .key_manager
686            .get_signing_key(approving_key_id)
687            .ok_or_else(|| super::errors::UserError::KeyNotFound {
688                key_id: approving_key_id.to_string(),
689            })?;
690
691        // Delegate to Sync layer with the user-provided key
692        // The Sync layer will validate permissions when committing the transaction
693        let key = DatabaseKey::new(signing_key.clone());
694        sync.approve_bootstrap_request_with_key(request_id, &key)
695            .await?;
696
697        Ok(())
698    }
699
700    /// Reject a bootstrap request.
701    ///
702    /// This method marks the request as rejected. The requesting device will not
703    /// be granted access to the target database. Requires Admin permission on the
704    /// target database to prevent unauthorized users from disrupting the bootstrap protocol.
705    ///
706    /// # Arguments
707    /// * `sync` - Mutable reference to the Instance's Sync object
708    /// * `request_id` - The unique identifier of the request to reject
709    /// * `rejecting_key_id` - The ID of this user's key (for permission validation and audit trail)
710    ///
711    /// # Returns
712    /// Result indicating success or failure of the rejection operation
713    ///
714    /// # Errors
715    /// - Returns an error if the user doesn't own the specified rejecting key
716    /// - Returns an error if the request doesn't exist or isn't pending
717    /// - Returns an error if the rejecting key lacks Admin permission on the target database
718    pub async fn reject_bootstrap_request(
719        &self,
720        sync: &Sync,
721        request_id: &str,
722        rejecting_key_id: &PublicKey,
723    ) -> Result<()> {
724        // Get the signing key from the key manager
725        let signing_key = self
726            .key_manager
727            .get_signing_key(rejecting_key_id)
728            .ok_or_else(|| super::errors::UserError::KeyNotFound {
729                key_id: rejecting_key_id.to_string(),
730            })?;
731
732        // Delegate to Sync layer with the user-provided key
733        // The Sync layer will validate Admin permission on the target database
734        let key = DatabaseKey::new(signing_key.clone());
735        sync.reject_bootstrap_request_with_key(request_id, &key)
736            .await?;
737
738        Ok(())
739    }
740
741    /// Request access to a database from a peer (bootstrap sync).
742    ///
743    /// This convenience method initiates a bootstrap sync request to access a database
744    /// that this user doesn't have locally yet. The user's key will be sent to the peer
745    /// to request the specified permission level.
746    ///
747    /// This is useful for multi-device scenarios where a user wants to access their
748    /// existing database from a new device, or when requesting access to a database
749    /// shared by another user.
750    ///
751    /// # Arguments
752    /// * `sync` - Reference to the Instance's Sync object
753    /// * `ticket` - A ticket containing the database ID and address hints
754    /// * `key_id` - The ID of this user's key to use for the request
755    /// * `requested_permission` - The permission level being requested
756    ///
757    /// # Returns
758    /// Result indicating success or failure of the bootstrap request
759    ///
760    /// # Errors
761    /// - Returns an error if the user doesn't own the specified key
762    /// - Returns an error if all addresses in the ticket fail
763    /// - Returns an error if the bootstrap sync fails
764    ///
765    /// # Example
766    /// ```rust,ignore
767    /// // Request write access to a shared database
768    /// let user_key_id = user.get_default_key()?;
769    /// let ticket: DatabaseTicket = "eidetica:?db=sha256:abc...&pr=http:192.168.1.1:8080".parse()?;
770    /// user.request_database_access(
771    ///     &sync,
772    ///     &ticket,
773    ///     &user_key_id,
774    ///     Permission::Write(5),
775    /// ).await?;
776    ///
777    /// // After approval, the database can be opened
778    /// let database = user.open_database(ticket.database_id())?;
779    /// ```
780    pub async fn request_database_access(
781        &self,
782        sync: &Sync,
783        ticket: &DatabaseTicket,
784        key_id: &PublicKey,
785        requested_permission: Permission,
786    ) -> Result<()> {
787        if self.key_manager.get_signing_key(key_id).is_none() {
788            return Err(super::errors::UserError::KeyNotFound {
789                key_id: key_id.to_string(),
790            }
791            .into());
792        }
793
794        let key_name = key_id.to_string();
795
796        sync.bootstrap_with_ticket(ticket, key_id, &key_name, requested_permission)
797            .await
798    }
799
800    // === Tracked Databases ===
801
802    /// Track a database, adding it to this user's list with auto-discovery of SigKeys.
803    ///
804    /// This method adds an existing database to your tracked list, or updates it if
805    /// already tracked (upsert behavior).
806    ///
807    /// When tracking:
808    /// 1. Uses Database::find_sigkeys() to discover which SigKey the user can use
809    /// 2. Automatically selects the SigKey with highest permission
810    /// 3. Stores the key mapping and sync settings
811    ///
812    /// The sync_settings indicate your sync preferences, but do not automatically
813    /// configure sync. Use the Sync module's peer and tree methods to set up actual
814    /// sync relationships.
815    ///
816    /// # Arguments
817    /// * `database_id` - ID of the database to track
818    /// * `key_id` - Which user key to use for this database
819    /// * `sync_settings` - Sync preferences for this database
820    ///
821    /// # Returns
822    /// Result indicating success or failure
823    ///
824    /// # Errors
825    /// - Returns `NoSigKeyFound` if no SigKey can be found for the specified key
826    /// - Returns `KeyNotFound` if the specified key_id doesn't exist
827    pub async fn track_database(
828        &mut self,
829        database_id: impl Into<ID>,
830        key_id: &PublicKey,
831        sync_settings: SyncSettings,
832    ) -> Result<()> {
833        let tracked = TrackedDatabase {
834            database_id: database_id.into(),
835            key_id: key_id.clone(),
836            sync_settings,
837        };
838        // Single transaction for all operations
839        let tx = self.user_database.new_transaction().await?;
840        let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
841
842        // Use database ID as the key - check if it already exists (O(1))
843        let db_id_key = tracked.database_id.to_string();
844        let existing = databases_table.get(&db_id_key).await.ok();
845
846        // Determine if we need to validate and setup key mapping
847        let needs_key_validation = match &existing {
848            Some(existing) => existing.key_id != tracked.key_id, // Key changed
849            None => true,                                        // New database
850        };
851
852        // Validate key and set up mapping if needed
853        if needs_key_validation {
854            self.validate_and_map_key_in_txn(&tx, &tracked.database_id, &tracked.key_id)
855                .await?;
856        }
857
858        // Store using database ID as explicit key (not using insert's auto-generated UUID)
859        databases_table.set(&db_id_key, tracked).await?;
860
861        // Single commit for all changes
862        tx.commit().await?;
863
864        // Update sync system to immediately recompute combined settings
865        // This ensures automatic sync works right away, without waiting for background worker
866        if let Some(sync) = self.instance.sync() {
867            // Auto-sync user tracking if not already synced
868            // This is idempotent - safe to call multiple times
869            sync.sync_user(&self.user_uuid, self.user_database.root_id())
870                .await?;
871        }
872
873        Ok(())
874    }
875
876    /// List all tracked databases.
877    ///
878    /// Returns all databases this user has added to their tracked list.
879    ///
880    /// # Returns
881    /// Vector of TrackedDatabase entries
882    pub async fn databases(&self) -> Result<Vec<TrackedDatabase>> {
883        let databases_table = self
884            .user_database
885            .get_store_viewer::<Table<TrackedDatabase>>("databases")
886            .await?;
887
888        // Get all entries from the table (returns Vec<(key, value)>)
889        let all_entries = databases_table.search(|_| true).await?;
890
891        // Extract just the values
892        let tracked: Vec<TrackedDatabase> = all_entries.into_iter().map(|(_key, db)| db).collect();
893
894        Ok(tracked)
895    }
896
897    /// Get a specific tracked database by ID.
898    ///
899    /// # Arguments
900    /// * `database_id` - The ID of the database
901    ///
902    /// # Returns
903    /// The TrackedDatabase if it's in the user's tracked list
904    ///
905    /// # Errors
906    /// Returns `DatabaseNotTracked` if the database is not in the user's list
907    pub async fn database(&self, database_id: &ID) -> Result<TrackedDatabase> {
908        let databases_table = self
909            .user_database()
910            .get_store_viewer::<Table<TrackedDatabase>>("databases")
911            .await?;
912
913        // Direct O(1) lookup using database ID as key
914        let db_id_key = database_id.to_string();
915        databases_table.get(&db_id_key).await.map_err(|_| {
916            UserError::DatabaseNotTracked {
917                database_id: database_id.to_string(),
918            }
919            .into()
920        })
921    }
922
923    /// Stop tracking a database.
924    ///
925    /// This removes the database from the user's tracked list.
926    /// It does not delete the database itself, remove key mappings, or delete any data.
927    ///
928    /// # Arguments
929    /// * `database_id` - The ID of the database to stop tracking
930    ///
931    /// # Errors
932    /// Returns `DatabaseNotTracked` if the database is not in the user's list
933    pub async fn untrack_database(&mut self, database_id: &ID) -> Result<()> {
934        let tx = self.user_database.new_transaction().await?;
935        let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
936
937        // Direct O(1) delete using database ID as key
938        let db_id_key = database_id.to_string();
939
940        // Verify it exists before deleting
941        if databases_table.get(&db_id_key).await.is_err() {
942            return Err(UserError::DatabaseNotTracked {
943                database_id: database_id.to_string(),
944            }
945            .into());
946        }
947
948        // Delete using database ID as key
949        databases_table.delete(&db_id_key).await?;
950        tx.commit().await?;
951
952        Ok(())
953    }
954}