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

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//! - **`open_database_with_key()`** - Open with an explicitly chosen user key
14//! - **`find_database()`** - Search for databases by name
15//!
16//! ## Tracked Databases
17//!
18//! Manage your personal list of tracked databases:
19//!
20//! - **`databases()`** - List all tracked databases
21//! - **`database()`** - Get a specific tracked database
22//! - **`track_database()`** - Add or update a tracked database (upsert)
23//! - **`untrack_database()`** - Remove a database from your tracked list
24//! - **`enable_sync()` / `disable_sync()`** - Toggle this user's sync preference for a tracked database
25//! - **`is_sync_enabled()`** - Check this user's sync preference for a database
26//! - **`share()`** - Atomically enable sync and build a `DatabaseTicket` for handoff
27//!
28//! ## Key-Database Mappings
29//!
30//! Control which keys access which databases:
31//!
32//! - **`map_key()`** - Map a key to a SigKey identifier for a database
33//! - **`key_mapping()`** - Get the SigKey mapping for a key-database pair
34//! - **`find_key()`** - Find which key can access a database
35//!
36//! This explicit approach ensures predictable behavior and avoids ambiguity about which
37//! keys have access to which databases.
38
39use std::collections::HashMap;
40
41use std::sync::Arc;
42
43use super::{UserKeyManager, admin::InstanceAdmin, types::UserInfo};
44use crate::{
45    Database, Error, Instance, Result, Transaction,
46    auth::{Permission, SigKey, crypto::PublicKey},
47    crdt::Doc,
48    database::DatabaseKey,
49    entry::ID,
50    instance::{InstanceError, backend::Backend},
51    store::Table,
52    sync::{BootstrapRequest, DatabaseTicket, Sync, SyncError},
53    user::{SyncSettings, TrackedDatabase, UserError},
54};
55
56mod builder;
57#[cfg(test)]
58mod tests;
59
60pub use builder::DatabaseBuilder;
61
62/// User session object, returned after successful login
63///
64/// Represents an authenticated user with decrypted private keys loaded in memory.
65/// The User struct provides access to key management, tracked databases, and
66/// bootstrap approval operations.
67pub struct User {
68    /// Stable internal user UUID (Table primary key)
69    user_uuid: String,
70
71    /// Username (login identifier)
72    username: String,
73
74    /// User's private database (contains encrypted keys and tracked databases)
75    user_database: Database,
76
77    /// Instance reference for database operations
78    instance: Instance,
79
80    /// Decrypted user keys (in memory only during session)
81    key_manager: UserKeyManager,
82
83    /// User info (cached from _users database)
84    user_info: UserInfo,
85}
86
87impl std::fmt::Debug for User {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("User")
90            .field("user_uuid", &self.user_uuid)
91            .field("username", &self.username)
92            .field("user_database", &self.user_database)
93            .field("instance", &self.instance)
94            .field("key_manager", &"<KeyManager [sensitive]>")
95            .field("user_info", &self.user_info)
96            .finish()
97    }
98}
99
100impl User {
101    /// Create a new User session
102    ///
103    /// This is an internal constructor used after successful login.
104    /// Use `Instance::login_user()` to create a User session.
105    ///
106    /// # Arguments
107    /// * `user_uuid` - Internal UUID (Table primary key)
108    /// * `user_info` - User information from _users database
109    /// * `user_database` - The user's private database
110    /// * `instance` - Instance reference
111    /// * `key_manager` - Initialized key manager with decrypted keys
112    #[allow(dead_code)]
113    pub(crate) fn new(
114        user_uuid: String,
115        user_info: UserInfo,
116        user_database: Database,
117        instance: Instance,
118        key_manager: UserKeyManager,
119    ) -> Self {
120        Self {
121            user_uuid,
122            username: user_info.username.clone(),
123            user_database,
124            instance,
125            key_manager,
126            user_info,
127        }
128    }
129
130    // === Basic Session Methods ===
131
132    /// Get the internal user UUID (stable identifier)
133    pub fn user_uuid(&self) -> &str {
134        &self.user_uuid
135    }
136
137    /// Get the username (login identifier)
138    pub fn username(&self) -> &str {
139        &self.username
140    }
141
142    /// Get a reference to the user's database
143    pub fn user_database(&self) -> &Database {
144        &self.user_database
145    }
146
147    /// Get a reference to the backend seam.
148    pub fn backend(&self) -> &Arc<dyn Backend> {
149        self.instance.backend()
150    }
151
152    /// Get a reference to the user info
153    pub fn user_info(&self) -> &UserInfo {
154        &self.user_info
155    }
156
157    /// Whether this user is an instance admin.
158    ///
159    /// "Instance admin" means the user's default pubkey holds `Admin` in the
160    /// `_users` system database's `auth_settings`. The `admin`/`admin` user
161    /// created during instance bootstrap is auto-promoted; subsequent users
162    /// land as non-admins until promoted via
163    /// [`InstanceAdmin::grant_instance_admin`](crate::user::InstanceAdmin::grant_instance_admin).
164    ///
165    /// Returns `false` if the key resolution fails (key not in auth_settings,
166    /// non-Admin permission, etc.). For the capability handle rather than a
167    /// bool, use [`Self::admin`].
168    pub async fn is_admin(&self) -> Result<bool> {
169        self.admin_check().await
170    }
171
172    /// Obtain the instance-admin capability view.
173    ///
174    /// Returns an [`InstanceAdmin`] only if this user's default key holds
175    /// `Admin` on the `_users` system database. All admin-gated operations
176    /// (creating users, listing users, promoting admins) live on the
177    /// returned view, so the privilege boundary is explicit at the call site
178    /// and those operations need no further permission check of their own.
179    ///
180    /// # Errors
181    /// - [`UserError::InsufficientPermissions`] if this user is not an
182    ///   instance admin.
183    pub async fn admin(&self) -> Result<InstanceAdmin<'_>> {
184        if self.admin_check().await? {
185            Ok(InstanceAdmin::new(self))
186        } else {
187            Err(UserError::InsufficientPermissions.into())
188        }
189    }
190
191    /// Single source of truth for "is this user an instance admin".
192    ///
193    /// Reads the `_users` `auth_settings` snapshot via the user's **session
194    /// key** (not the device key), so it behaves identically on local and
195    /// remote instances. Returns `Ok(false)` for a non-admin or unresolved
196    /// key; propagates real infrastructure errors. Shared by
197    /// [`Self::is_admin`] and [`Self::admin`].
198    async fn admin_check(&self) -> Result<bool> {
199        let default_pubkey =
200            self.key_manager
201                .get_default_key_id()
202                .ok_or_else(|| UserError::KeyNotFound {
203                    key_id: "<default>".to_string(),
204                })?;
205        let signing_key = self.default_signing_key()?;
206        let users_db = self.instance.users_db_for_session(&signing_key).await?;
207        let tx = users_db.new_transaction().await?;
208        let settings = tx.get_settings()?;
209        let auth = settings.auth_snapshot().await?;
210        match auth.get_key_by_pubkey(&default_pubkey) {
211            Ok(key) => Ok(matches!(key.permissions(), Permission::Admin(_))),
212            Err(_) => Ok(false),
213        }
214    }
215
216    /// The user's default signing key (decrypted, in-session).
217    ///
218    /// Shared by admin / system-DB paths that must sign as the user
219    /// directly, rather than via a tracked-database SigKey mapping.
220    pub(crate) fn default_signing_key(&self) -> Result<crate::auth::crypto::PrivateKey> {
221        let key_id =
222            self.key_manager
223                .get_default_key_id()
224                .ok_or_else(|| UserError::KeyNotFound {
225                    key_id: "<default>".to_string(),
226                })?;
227        self.key_manager
228            .get_signing_key(&key_id)
229            .cloned()
230            .ok_or_else(|| {
231                UserError::KeyNotFound {
232                    key_id: key_id.to_string(),
233                }
234                .into()
235            })
236    }
237
238    /// Instance reference (internal — for admin / system-DB helpers).
239    pub(crate) fn instance(&self) -> &Instance {
240        &self.instance
241    }
242
243    /// Logout (consumes self and clears decrypted keys from memory)
244    ///
245    /// After logout, all decrypted keys are zeroized and the session is ended.
246    /// Keys are automatically cleared when the User is dropped.
247    pub fn logout(self) -> Result<()> {
248        // Consume self, all keys are stored in other Types that zeroize themselves on drop
249        Ok(())
250    }
251
252    // === Key Manager Access (Internal) ===
253
254    /// Get a reference to the key manager (for internal use)
255    #[allow(dead_code)]
256    pub(crate) fn key_manager(&self) -> &UserKeyManager {
257        &self.key_manager
258    }
259
260    /// Get a mutable reference to the key manager (for internal use)
261    #[allow(dead_code)]
262    pub(crate) fn key_manager_mut(&mut self) -> &mut UserKeyManager {
263        &mut self.key_manager
264    }
265
266    // === Database Operations (User Context) ===
267
268    /// Start building a new database via the chainable [`DatabaseBuilder`] API.
269    ///
270    /// The builder collects settings, key policy, and store initializers, then
271    /// produces a fully-initialized database in a single genesis entry when
272    /// [`DatabaseBuilder::build`] is called.
273    ///
274    /// For the lower-level direct constructor see [`Self::create_database`].
275    pub fn new_database(&mut self) -> DatabaseBuilder<'_> {
276        DatabaseBuilder::new(self)
277    }
278
279    /// Create a new database with explicit key selection.
280    ///
281    /// This method requires you to specify which key should be used to create and manage
282    /// the database, providing explicit control over key-database relationships.
283    ///
284    /// # Arguments
285    /// * `settings` - Initial database settings (metadata, name, etc.)
286    /// * `key_id` - The ID of the key to use for this database (public key string)
287    ///
288    /// # Returns
289    /// The created Database
290    ///
291    /// # Errors
292    /// - Returns an error if the specified key_id doesn't exist
293    /// - Returns an error if the key cannot be retrieved
294    ///
295    /// # Example
296    /// ```rust,ignore
297    /// // Get available keys
298    /// let keys = user.list_keys()?;
299    /// let key_id = &keys[1]; // Use the second key
300    ///
301    /// // Create database with explicit key selection
302    /// let mut settings = Doc::new();
303    /// settings.set("name", "My Database");
304    /// let database = user.new_database(settings, key_id)?;
305    /// ```
306    pub async fn create_database(&mut self, settings: Doc, key_id: &PublicKey) -> Result<Database> {
307        self.create_database_with_init(settings, key_id, async |_| Ok(()))
308            .await
309    }
310
311    /// Creates a new database with an initialization callback that runs inside
312    /// the genesis transaction.
313    ///
314    /// This is the underlying constructor used by [`Self::create_database`] and by
315    /// [`Self::new_database`] (the builder API). The callback receives the
316    /// genesis transaction after `_settings` and `_root` have been staged but
317    /// before commit, allowing additional subtrees to be written into the same
318    /// entry that establishes the database root. See
319    /// [`Database::create_with_init`] for details on the atomicity guarantee.
320    ///
321    /// After the genesis entry commits, this method performs the standard
322    /// user-side tracking write (key-database mapping, `TrackedDatabase` entry)
323    /// in a separate transaction on the user's own system database.
324    pub async fn create_database_with_init<F>(
325        &mut self,
326        settings: Doc,
327        key_id: &PublicKey,
328        init: F,
329    ) -> Result<Database>
330    where
331        F: AsyncFnOnce(&Transaction) -> Result<()>,
332    {
333        use crate::user::types::{SyncSettings, UserKey};
334
335        // Get the signing key from UserKeyManager
336        let signing_key = self
337            .key_manager
338            .get_signing_key(key_id)
339            .ok_or_else(|| UserError::KeyNotFound {
340                key_id: key_id.to_string(),
341            })?
342            .clone();
343
344        // Create the database with the provided key directly
345        let database =
346            Database::create_with_init(&self.instance, signing_key, settings, init).await?;
347
348        // Store the mapping in UserKey and track the database
349        let tx = self.user_database.new_transaction().await?;
350        let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
351
352        // Find the key metadata in the database
353        let (uuid_primary_key, mut metadata) = keys_table
354            .search(|uk| &uk.key_id == key_id)
355            .await?
356            .into_iter()
357            .next()
358            .ok_or_else(|| UserError::KeyNotFound {
359                key_id: key_id.to_string(),
360            })?;
361
362        // Add the database sigkey mapping (None = default pubkey identity)
363        metadata
364            .database_sigkeys
365            .insert(database.root_id().clone(), None);
366
367        // Update the key in user database using the UUID primary key
368        keys_table.set(&uuid_primary_key, metadata.clone()).await?;
369
370        // Also track the database in the databases table
371        let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
372        let tracked = TrackedDatabase {
373            database_id: database.root_id().clone(),
374            key_id: key_id.clone(),
375            sync_settings: SyncSettings::disabled(),
376        };
377        databases_table
378            .set(&database.root_id().to_string(), tracked)
379            .await?;
380
381        tx.commit().await?;
382
383        // Update the in-memory key manager with the updated metadata
384        self.key_manager.add_key(metadata)?;
385
386        Ok(database)
387    }
388
389    /// Open an existing database by its root ID using this user's keys.
390    ///
391    /// This method automatically:
392    /// 1. Finds an appropriate key that has access to the database
393    /// 2. Retrieves the decrypted SigningKey from the UserKeyManager
394    /// 3. Gets the SigKey mapping for this database
395    /// 4. Creates a Database instance configured with the user's key
396    ///
397    /// The returned Database will use the user's provided key for all operations,
398    /// without requiring backend key lookups.
399    ///
400    /// # Arguments
401    /// * `root_id` - The root entry ID of the database
402    ///
403    /// # Returns
404    /// The opened Database configured to use this user's keys
405    ///
406    /// # Errors
407    /// - Returns an error if no key is found for the database
408    /// - Returns an error if no SigKey mapping exists
409    /// - Returns an error if the key is not in the UserKeyManager
410    pub async fn open_database(&self, root_id: &ID) -> Result<Database> {
411        // Find an appropriate key for this database
412        let key_id =
413            self.find_key(root_id)?
414                .ok_or_else(|| super::errors::UserError::NoKeyForDatabase {
415                    database_id: root_id.clone(),
416                })?;
417
418        self.open_database_with_key(root_id, &key_id).await
419    }
420
421    /// Open an existing database with an explicitly chosen key.
422    ///
423    /// Equivalent to `open_database`, but selects the user's signing key by
424    /// public key instead of relying on `find_key`'s iteration order. Use this
425    /// when the user holds multiple authorized keys for a database and you
426    /// need writes signed by a specific one (e.g. agent-as-signer scenarios
427    /// where cryptographic provenance matters, not just authorization).
428    ///
429    /// The `key_id` is purely a selector into this user's `UserKeyManager`;
430    /// no crypto material is passed in.
431    ///
432    /// # Arguments
433    /// * `root_id` - The root entry ID of the database
434    /// * `key_id` - Public key of the user-held key to sign with
435    ///
436    /// # Returns
437    /// The opened Database configured to use the specified key
438    ///
439    /// # Errors
440    /// - Returns an error if the root entry does not exist
441    /// - Returns an error if the user does not hold a key with that pubkey
442    /// - Returns an error if the key has no SigKey mapping for this database
443    pub async fn open_database_with_key(
444        &self,
445        root_id: &ID,
446        key_id: &PublicKey,
447    ) -> Result<Database> {
448        // Get the SigningKey from UserKeyManager
449        let signing_key = self.key_manager.get_signing_key(key_id).ok_or_else(|| {
450            super::errors::UserError::KeyNotFound {
451                key_id: key_id.to_string(),
452            }
453        })?;
454
455        // Get the SigKey mapping for this database
456        let sigkey = self.key_mapping(key_id, root_id)?.ok_or_else(|| {
457            super::errors::UserError::NoSigKeyMapping {
458                key_id: key_id.to_string(),
459                database_id: root_id.clone(),
460            }
461        })?;
462
463        // Create Database with user-provided key using resolved SigKey identity.
464        //
465        // On a connected (remote) instance, the read path must travel as the
466        // user's per-DB identity so the daemon's per-tree gate sees a key the
467        // tree actually authorises. `Database::open` would clone the
468        // instance's session backend, which on a remote instance carries the
469        // connection's login pubkey — and the user's login key is not a member
470        // of every tree they hold a per-DB key for. Route through
471        // `Database::open_remote` with the per-DB
472        // identity instead, after proving possession of the per-DB key to
473        // the daemon so the identity sits in the connection's session
474        // keyset.
475        let key = DatabaseKey::with_identity(signing_key.clone(), sigkey.clone());
476        // A SigKey mapping exists (resolved above), so the database is one this
477        // user has requested access to. If its root entry isn't present, the
478        // access is still pending — the bootstrap request hasn't been approved or
479        // the database hasn't synced yet. Surface that explicitly instead of a
480        // bare backend "not found", on both the remote and local open paths.
481        #[cfg(all(unix, feature = "service"))]
482        if let Some(conn) = self.instance.remote_connection() {
483            conn.register_session_key(signing_key).await?;
484            return match Database::open_remote(&self.instance, conn, root_id, sigkey).await {
485                Ok(database) => Ok(database.with_key(key)),
486                Err(e) if e.is_not_found() => {
487                    Err(super::errors::UserError::DatabaseAccessPending {
488                        database_id: root_id.clone(),
489                    }
490                    .into())
491                }
492                Err(e) => Err(e),
493            };
494        }
495        match Database::open(&self.instance, root_id).await {
496            Ok(database) => Ok(database.with_key(key)),
497            Err(e) if e.is_not_found() => Err(super::errors::UserError::DatabaseAccessPending {
498                database_id: root_id.clone(),
499            }
500            .into()),
501            Err(e) => Err(e),
502        }
503    }
504
505    /// Find databases by name among the user's tracked databases.
506    ///
507    /// Searches only the databases this user has tracked for those matching the given name.
508    ///
509    /// # Arguments
510    /// * `name` - Database name to search for
511    ///
512    /// # Returns
513    /// Vector of matching databases from the user's tracked list
514    pub async fn find_database(&self, name: impl AsRef<str>) -> Result<Vec<Database>> {
515        let name = name.as_ref();
516        let tracked = self.databases().await?;
517        let mut matching = Vec::new();
518
519        for tracked_db in tracked {
520            if let Ok(database) = self.open_database(&tracked_db.database_id).await
521                && let Ok(db_name) = database.get_name().await
522                && db_name == name
523            {
524                matching.push(database);
525            }
526        }
527
528        if matching.is_empty() {
529            Err(UserError::DatabaseNotFoundByName {
530                name: name.to_string(),
531            }
532            .into())
533        } else {
534            Ok(matching)
535        }
536    }
537
538    /// Find which key can access a database.
539    ///
540    /// Searches this user's keys to find one that can access the specified database.
541    /// Considers the SigKey mappings stored in user key metadata.
542    ///
543    /// Returns the key_id of a suitable key, preferring keys with mappings for this database.
544    ///
545    /// # Arguments
546    /// * `database_id` - The ID of the database
547    ///
548    /// # Returns
549    /// Some(key_id) if a suitable key is found, None if no keys can access this database
550    pub fn find_key(&self, database_id: &ID) -> Result<Option<PublicKey>> {
551        // Iterate through all keys and find ones with SigKey mappings for this database
552        for key_id in self.key_manager.list_key_ids() {
553            if let Some(metadata) = self.key_manager.get_key_metadata(&key_id)
554                && metadata.database_sigkeys.contains_key(database_id)
555            {
556                return Ok(Some(key_id));
557            }
558        }
559
560        // No key found with mapping for this database
561        Ok(None)
562    }
563
564    /// Get the resolved SigKey mapping for a key in a specific database.
565    ///
566    /// Users map their private keys to SigKey identifiers on a per-database basis.
567    /// This retrieves the resolved SigKey that a specific key uses in
568    /// a specific database's authentication settings.
569    ///
570    /// Internally, `None` in the stored mapping means "default pubkey identity",
571    /// which this method resolves to the concrete `SigKey::from_pubkey(...)` value.
572    ///
573    /// # Arguments
574    /// * `key_id` - The user's key identifier
575    /// * `database_id` - The database ID
576    ///
577    /// # Returns
578    /// `Ok(Some(sigkey))` if a mapping exists (resolved to concrete SigKey),
579    /// `Ok(None)` if no mapping is configured for this database
580    ///
581    /// # Errors
582    /// Returns an error if the key_id doesn't exist in the UserKeyManager
583    pub fn key_mapping(&self, key_id: &PublicKey, database_id: &ID) -> Result<Option<SigKey>> {
584        let metadata = self.key_manager.get_key_metadata(key_id).ok_or_else(|| {
585            super::errors::UserError::KeyNotFound {
586                key_id: key_id.to_string(),
587            }
588        })?;
589
590        match metadata.database_sigkeys.get(database_id) {
591            None => Ok(None), // no mapping exists
592            Some(None) => {
593                // Default: pubkey identity derived directly from key_id
594                Ok(Some(SigKey::from_pubkey(key_id)))
595            }
596            Some(Some(sigkey)) => Ok(Some(sigkey.clone())),
597        }
598    }
599
600    /// Map a key to a SigKey identity for a specific database.
601    ///
602    /// Registers that this user's key should be used with a specific SigKey identity
603    /// when interacting with a database. This is typically used when a user has been
604    /// granted access to a database and needs to configure their local key to work with it.
605    ///
606    /// If the provided SigKey matches the default pubkey identity for this key,
607    /// it is normalized to `None` internally (compact storage for the common case).
608    ///
609    /// # Multi-Key Support
610    ///
611    /// **Note**: A database may have mappings to multiple keys. This is useful for
612    /// multi-device scenarios where the same user wants to access a database from
613    /// different devices, each with their own key.
614    ///
615    /// # Arguments
616    /// * `key_id` - The user's key identifier (public key)
617    /// * `database_id` - The database ID
618    /// * `sigkey` - The SigKey identity to use for this database
619    ///
620    /// # Errors
621    /// Returns an error if the key_id doesn't exist in the user database
622    pub async fn map_key(
623        &mut self,
624        key_id: &PublicKey,
625        database_id: &ID,
626        sigkey: SigKey,
627    ) -> Result<()> {
628        let tx = self.user_database.new_transaction().await?;
629        self.map_key_in_txn(&tx, key_id, database_id, sigkey)
630            .await?;
631        tx.commit().await?;
632        Ok(())
633    }
634
635    /// Internal helper: Add a SigKey mapping within an existing transaction
636    ///
637    /// This is used internally by methods that manage their own transactions.
638    /// For external use, call `map_key()` instead.
639    ///
640    /// Normalizes the stored value: if the sigkey matches the default pubkey
641    /// identity for this key, stores `None` instead of `Some(sigkey)`.
642    async fn map_key_in_txn(
643        &mut self,
644        tx: &Transaction,
645        key_id: &PublicKey,
646        database_id: &ID,
647        sigkey: SigKey,
648    ) -> Result<()> {
649        use crate::store::Table;
650        use crate::user::types::UserKey;
651
652        let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
653
654        // Find the key metadata in the database
655        let (uuid_primary_key, mut metadata) = keys_table
656            .search(|uk| &uk.key_id == key_id)
657            .await?
658            .into_iter()
659            .next()
660            .ok_or_else(|| super::errors::UserError::KeyNotFound {
661                key_id: key_id.to_string(),
662            })?;
663
664        // Normalize: if the sigkey matches the default pubkey identity, store None
665        let default_sigkey = SigKey::from_pubkey(key_id);
666        let stored = if sigkey == default_sigkey {
667            None
668        } else {
669            Some(sigkey)
670        };
671
672        // Add the database sigkey mapping
673        metadata
674            .database_sigkeys
675            .insert(database_id.clone(), stored);
676
677        // Update the key in user database using the UUID primary key
678        keys_table.set(&uuid_primary_key, metadata.clone()).await?;
679
680        // Update the in-memory key manager with the updated metadata
681        self.key_manager.add_key(metadata)?;
682
683        Ok(())
684    }
685
686    /// Internal helper: Validate key and set up SigKey mapping within an existing transaction
687    ///
688    /// This validates that a key exists and has access to a database, discovers the appropriate
689    /// SigKey, and creates the mapping. Used by track_database (which has upsert behavior).
690    async fn validate_and_map_key_in_txn(
691        &mut self,
692        tx: &Transaction,
693        database_id: &ID,
694        key_id: &PublicKey,
695    ) -> Result<()> {
696        // Verify the key exists
697        if self.key_manager.get_signing_key(key_id).is_none() {
698            return Err(UserError::KeyNotFound {
699                key_id: key_id.to_string(),
700            }
701            .into());
702        }
703
704        // Discover available SigKeys for this public key
705        let available_sigkeys = Database::find_sigkeys(&self.instance, database_id, key_id).await?;
706
707        if available_sigkeys.is_empty() {
708            return Err(UserError::NoSigKeyFound {
709                key_id: key_id.to_string(),
710                database_id: database_id.clone(),
711            }
712            .into());
713        }
714
715        // Select the first SigKey (highest permission, since find_sigkeys returns sorted list)
716        let (sigkey, _permission) = &available_sigkeys[0];
717
718        // Store the discovered SigKey directly (map_key_in_txn normalizes to None if default)
719        self.map_key_in_txn(tx, key_id, database_id, sigkey.clone())
720            .await?;
721
722        Ok(())
723    }
724
725    // === Key Management (User Context) ===
726
727    /// Add a new private key to this user's keyring.
728    ///
729    /// Generates a new Ed25519 keypair, encrypts it (for password-protected users)
730    /// or stores it unencrypted (for passwordless users), and adds it to the user's
731    /// key database.
732    ///
733    /// # Arguments
734    /// * `display_name` - Optional display name for the key
735    ///
736    /// # Returns
737    /// The key ID (public key string)
738    pub async fn add_private_key(&mut self, display_name: Option<&str>) -> Result<PublicKey> {
739        use crate::auth::crypto::generate_keypair;
740        use crate::store::Table;
741        use crate::user::types::{KeyStorage, UserKey};
742
743        // Generate new keypair
744        let (private_key, public_key) = generate_keypair();
745
746        // Get current timestamp using the instance's clock
747        let timestamp = self.instance.clock().now_secs();
748
749        // Prepare UserKey based on encryption type
750        let user_key = if let Some(encryption_key) = self.key_manager.encryption_key() {
751            // Password-protected user: encrypt the key
752            use crate::user::crypto::encrypt_private_key;
753            let (ciphertext, nonce) = encrypt_private_key(&private_key, encryption_key)?;
754
755            UserKey {
756                key_id: public_key.clone(),
757                storage: KeyStorage::Encrypted {
758                    algorithm: "aes-256-gcm".to_string(),
759                    ciphertext,
760                    nonce,
761                },
762                display_name: display_name.map(|s| s.to_string()),
763                created_at: timestamp,
764                last_used: None,
765                is_default: false, // New keys are not default
766                database_sigkeys: HashMap::new(),
767            }
768        } else {
769            // Passwordless user: store unencrypted
770            UserKey {
771                key_id: public_key.clone(),
772                storage: KeyStorage::Unencrypted { key: private_key },
773                display_name: display_name.map(|s| s.to_string()),
774                created_at: timestamp,
775                last_used: None,
776                is_default: false, // New keys are not default
777                database_sigkeys: HashMap::new(),
778            }
779        };
780
781        // Store in user database
782        let tx = self.user_database.new_transaction().await?;
783        let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
784        keys_table.insert(user_key.clone()).await?;
785        tx.commit().await?;
786
787        // Add to in-memory key manager
788        self.key_manager.add_key(user_key)?;
789
790        Ok(public_key)
791    }
792
793    /// List all key IDs owned by this user.
794    ///
795    /// Keys are returned sorted by creation timestamp (oldest first), making the
796    /// first key in the list the "default" key created when the user was set up.
797    ///
798    /// # Returns
799    /// Vector of PublicKeys sorted by creation time
800    pub fn list_keys(&self) -> Result<Vec<PublicKey>> {
801        Ok(self.key_manager.list_key_ids())
802    }
803
804    /// Get the default key.
805    ///
806    /// Returns the key marked as is_default=true, or falls back to the oldest key
807    /// by creation timestamp if no default is explicitly set.
808    ///
809    /// # Returns
810    /// The PublicKey of the default key
811    ///
812    /// # Errors
813    /// Returns an error if no keys exist
814    pub fn get_default_key(&self) -> Result<PublicKey> {
815        self.key_manager
816            .get_default_key_id()
817            .ok_or_else(|| Error::from(InstanceError::AuthenticationRequired))
818    }
819
820    /// Get the display name set for a key.
821    ///
822    /// Display names are caller-supplied human-readable labels passed to
823    /// `add_private_key`. They have no cryptographic significance and are
824    /// not unique across keys. Returns `None` if the user does not hold a
825    /// key with this pubkey, or if it was added without a display name.
826    ///
827    /// # Arguments
828    /// * `key_id` - Public key of a key held by this user
829    pub fn key_display_name(&self, key_id: &PublicKey) -> Option<&str> {
830        self.key_manager
831            .get_key_metadata(key_id)
832            .and_then(|metadata| metadata.display_name.as_deref())
833    }
834
835    /// Find user-held keys whose display name matches `name`.
836    ///
837    /// Display names are not unique, so this returns every key whose
838    /// `display_name` exactly matches the provided string. Keys without a
839    /// display name are never returned. Returns an empty `Vec` when there
840    /// are no matches.
841    ///
842    /// # Arguments
843    /// * `name` - Exact display name to match
844    pub fn find_keys_by_display_name(&self, name: &str) -> Vec<PublicKey> {
845        self.key_manager
846            .list_key_ids()
847            .into_iter()
848            .filter(|key_id| {
849                self.key_manager
850                    .get_key_metadata(key_id)
851                    .and_then(|metadata| metadata.display_name.as_deref())
852                    == Some(name)
853            })
854            .collect()
855    }
856
857    /// Get a signing key by its ID.
858    ///
859    /// Hands out key material from this session's unlocked key manager. Callers
860    /// that sign on the user's behalf need it — signing a sync request as the
861    /// key whose access they are claiming, for example.
862    ///
863    /// # Arguments
864    /// * `key_id` - The public key identifier
865    ///
866    /// # Returns
867    /// The PrivateKey if found
868    pub fn get_signing_key(&self, key_id: &PublicKey) -> Result<crate::auth::crypto::PrivateKey> {
869        self.key_manager
870            .get_signing_key(key_id)
871            .cloned()
872            .ok_or_else(|| {
873                UserError::KeyNotFound {
874                    key_id: key_id.to_string(),
875                }
876                .into()
877            })
878    }
879
880    // === Bootstrap Request Management (User Context) ===
881
882    /// Get all pending bootstrap requests from the sync system.
883    ///
884    /// This is a convenience method that requires the Instance's Sync to be initialized.
885    ///
886    /// # Arguments
887    /// * `sync` - Reference to the Instance's Sync object
888    ///
889    /// # Returns
890    /// A vector of (request_id, bootstrap_request) pairs for pending requests
891    pub async fn pending_bootstrap_requests(
892        &self,
893        sync: &Sync,
894    ) -> Result<Vec<(String, BootstrapRequest)>> {
895        sync.pending_bootstrap_requests().await
896    }
897
898    /// Approve a bootstrap request and add the requesting key to the target database.
899    ///
900    /// The approving key must have Admin permission on the target database.
901    ///
902    /// # Arguments
903    /// * `sync` - Mutable reference to the Instance's Sync object
904    /// * `request_id` - The unique identifier of the request to approve
905    /// * `approving_key_id` - The ID of this user's key to use for approval (must have Admin permission)
906    ///
907    /// # Returns
908    /// Result indicating success or failure of the approval operation
909    ///
910    /// # Errors
911    /// - Returns an error if the user doesn't own the specified approving key
912    /// - Returns an error if the approving key doesn't have Admin permission on the target database
913    /// - Returns an error if the request doesn't exist or isn't pending
914    /// - Returns an error if the key addition to the database fails
915    pub async fn approve_bootstrap_request(
916        &self,
917        sync: &Sync,
918        request_id: &str,
919        approving_key_id: &PublicKey,
920    ) -> Result<()> {
921        // Get the signing key from the key manager
922        let signing_key = self
923            .key_manager
924            .get_signing_key(approving_key_id)
925            .ok_or_else(|| super::errors::UserError::KeyNotFound {
926                key_id: approving_key_id.to_string(),
927            })?;
928
929        // Delegate to Sync layer with the user-provided key
930        // The Sync layer will validate permissions when committing the transaction
931        let key = DatabaseKey::new(signing_key.clone());
932        sync.approve_bootstrap_request_with_key(request_id, &key)
933            .await?;
934
935        Ok(())
936    }
937
938    /// Reject a bootstrap request.
939    ///
940    /// This method marks the request as rejected. The requesting device will not
941    /// be granted access to the target database. Requires Admin permission on the
942    /// target database to prevent unauthorized users from disrupting the bootstrap protocol.
943    ///
944    /// # Arguments
945    /// * `sync` - Mutable reference to the Instance's Sync object
946    /// * `request_id` - The unique identifier of the request to reject
947    /// * `rejecting_key_id` - The ID of this user's key (for permission validation and audit trail)
948    ///
949    /// # Returns
950    /// Result indicating success or failure of the rejection operation
951    ///
952    /// # Errors
953    /// - Returns an error if the user doesn't own the specified rejecting key
954    /// - Returns an error if the request doesn't exist or isn't pending
955    /// - Returns an error if the rejecting key lacks Admin permission on the target database
956    pub async fn reject_bootstrap_request(
957        &self,
958        sync: &Sync,
959        request_id: &str,
960        rejecting_key_id: &PublicKey,
961    ) -> Result<()> {
962        // Get the signing key from the key manager
963        let signing_key = self
964            .key_manager
965            .get_signing_key(rejecting_key_id)
966            .ok_or_else(|| super::errors::UserError::KeyNotFound {
967                key_id: rejecting_key_id.to_string(),
968            })?;
969
970        // Delegate to Sync layer with the user-provided key
971        // The Sync layer will validate Admin permission on the target database
972        let key = DatabaseKey::new(signing_key.clone());
973        sync.reject_bootstrap_request_with_key(request_id, &key)
974            .await?;
975
976        Ok(())
977    }
978
979    /// Request access to a database from a peer (bootstrap sync).
980    ///
981    /// This convenience method initiates a bootstrap sync request to access a database
982    /// that this user doesn't have locally yet. The user's key will be sent to the peer
983    /// to request the specified permission level.
984    ///
985    /// This is useful for multi-device scenarios where a user wants to access their
986    /// existing database from a new device, or when requesting access to a database
987    /// shared by another user.
988    ///
989    /// On a successful (already-authorized) request the database is synced and its
990    /// SigKey mapping is recorded, so [`open_database`](Self::open_database) works
991    /// immediately — no separate [`track_database`](Self::track_database) call is
992    /// needed just to open it. Note the database is tracked with sync **disabled**
993    /// by default; to keep it syncing in the background, call
994    /// [`track_database`](Self::track_database) (or [`enable_sync`](Self::enable_sync))
995    /// with your desired settings. Any settings already configured for the
996    /// database are preserved across a repeat request.
997    ///
998    /// When approval is still pending the request returns
999    /// [`SyncError::BootstrapPending`] but a provisional mapping is recorded;
1000    /// opening the database before approval then fails with
1001    /// [`UserError::DatabaseAccessPending`] rather than a cryptic backend error.
1002    ///
1003    /// # Arguments
1004    /// * `sync` - Reference to the Instance's Sync object
1005    /// * `ticket` - A ticket containing the database ID and address hints
1006    /// * `key_id` - The ID of this user's key to use for the request
1007    /// * `requested_permission` - The permission level being requested
1008    /// * `metadata` - Optional free-form context for the approver to inspect on
1009    ///   the stored bootstrap request when deciding whether to grant access
1010    ///
1011    /// # Returns
1012    /// Result indicating success or failure of the bootstrap request
1013    ///
1014    /// # Errors
1015    /// - Returns an error if the user doesn't own the specified key
1016    /// - Returns an error if all addresses in the ticket fail
1017    /// - Returns an error if the bootstrap sync fails
1018    ///
1019    /// # Example
1020    /// ```rust,ignore
1021    /// // Request write access to a shared database
1022    /// let user_key_id = user.get_default_key()?;
1023    /// let ticket: DatabaseTicket = "eidetica:?db=sha256:abc...&pr=http:192.168.1.1:8080".parse()?;
1024    /// user.request_database_access(
1025    ///     &sync,
1026    ///     &ticket,
1027    ///     &user_key_id,
1028    ///     Permission::Write(5),
1029    /// ).await?;
1030    ///
1031    /// // After approval, the database can be opened
1032    /// let database = user.open_database(ticket.database_id())?;
1033    /// ```
1034    pub async fn request_database_access(
1035        &mut self,
1036        sync: &Sync,
1037        ticket: &DatabaseTicket,
1038        key_id: &PublicKey,
1039        requested_permission: Permission,
1040        metadata: Option<Doc>,
1041    ) -> Result<()> {
1042        // The request is signed with this key, so the peer can tell an actual
1043        // key holder from someone naming a key they don't have.
1044        let signing_key = self
1045            .key_manager
1046            .get_signing_key(key_id)
1047            .ok_or_else(|| super::errors::UserError::KeyNotFound {
1048                key_id: key_id.to_string(),
1049            })?
1050            .clone();
1051
1052        let key_name = key_id.to_string();
1053        let database_id = ticket.database_id().clone();
1054
1055        let result = sync
1056            .bootstrap_with_ticket(
1057                ticket,
1058                &signing_key,
1059                &key_name,
1060                requested_permission,
1061                metadata,
1062            )
1063            .await;
1064
1065        self.record_database_access(&database_id, key_id, result)
1066            .await
1067    }
1068
1069    /// Record the User-layer SigKey mapping for a bootstrap whose network phase
1070    /// has already completed, given the [`Result`] returned by
1071    /// [`Sync::bootstrap_with_ticket`](crate::sync::Sync::bootstrap_with_ticket).
1072    ///
1073    /// [`request_database_access`](Self::request_database_access) is the usual
1074    /// entry point and performs the network bootstrap for you. This split-out
1075    /// method exists for callers that hold a coarse lock around the `User` (e.g.
1076    /// a daemon's per-session lock): they can run the network round-trip without
1077    /// the lock held and re-acquire it only for this cheap, local mapping write,
1078    /// so a slow or hung peer never blocks the rest of the session. The mapping
1079    /// semantics are identical to `request_database_access`.
1080    ///
1081    /// The `bootstrap_result` is consumed and its outcome re-raised unchanged so
1082    /// callers can react to [`SyncError::BootstrapPending`] et al.
1083    pub async fn record_database_access(
1084        &mut self,
1085        database_id: &ID,
1086        key_id: &PublicKey,
1087        bootstrap_result: Result<()>,
1088    ) -> Result<()> {
1089        // Bootstrap grants access at the sync layer (auth + entries) but does not
1090        // establish the User-layer SigKey mapping that `open_database`/`find_key`
1091        // rely on. Establish it here so a successful request leaves the database
1092        // openable — previously the caller had to call `track_database` manually,
1093        // and omitting it left the database unopenable ("No key found").
1094        match bootstrap_result {
1095            Ok(()) => {
1096                // Access was already authorized and the database is now synced, so
1097                // its auth settings are local: discover the real SigKey (which may
1098                // be a direct, global-wildcard, or delegated key) and record it.
1099                // Preserve any sync settings the caller already configured for this
1100                // database — a repeat request must not silently disable sync — and
1101                // fall back to the default only for a freshly-tracked database.
1102                let sync_settings = self
1103                    .database(database_id)
1104                    .await
1105                    .map(|tracked| tracked.sync_settings)
1106                    .unwrap_or_default();
1107                self.track_database(database_id.clone(), key_id, sync_settings)
1108                    .await?;
1109                Ok(())
1110            }
1111            Err(e) => {
1112                // Awaiting manual approval: the database isn't synced yet, so the
1113                // SigKey can't be discovered. On approval the key is added by
1114                // pubkey (see `Sync::approve_bootstrap_request_with_key`), so its
1115                // SigKey will be the default pubkey identity — record that mapping
1116                // provisionally. Until the database syncs, opening it surfaces
1117                // `UserError::DatabaseAccessPending`. The pending error is
1118                // re-raised unchanged so callers can react to it.
1119                if let Error::Sync(sync_err) = &e
1120                    && matches!(sync_err.as_ref(), SyncError::BootstrapPending { .. })
1121                {
1122                    self.map_key(key_id, database_id, SigKey::from_pubkey(key_id))
1123                        .await?;
1124                }
1125                Err(e)
1126            }
1127        }
1128    }
1129
1130    // === Tracked Databases ===
1131
1132    /// Track a database, adding it to this user's list with auto-discovery of SigKeys.
1133    ///
1134    /// This method adds an existing database to your tracked list, or updates it if
1135    /// already tracked (upsert behavior).
1136    ///
1137    /// When tracking:
1138    /// 1. Uses Database::find_sigkeys() to discover which SigKey the user can use
1139    /// 2. Automatically selects the SigKey with highest permission
1140    /// 3. Stores the key mapping and sync settings
1141    ///
1142    /// The sync_settings indicate your sync preferences, but do not automatically
1143    /// configure sync. Use the Sync module's peer and tree methods to set up actual
1144    /// sync relationships.
1145    ///
1146    /// # Arguments
1147    /// * `database_id` - ID of the database to track
1148    /// * `key_id` - Which user key to use for this database
1149    /// * `sync_settings` - Sync preferences for this database
1150    ///
1151    /// # Returns
1152    /// Result indicating success or failure
1153    ///
1154    /// # Errors
1155    /// - Returns `NoSigKeyFound` if no SigKey can be found for the specified key
1156    /// - Returns `KeyNotFound` if the specified key_id doesn't exist
1157    pub async fn track_database(
1158        &mut self,
1159        database_id: impl Into<ID>,
1160        key_id: &PublicKey,
1161        sync_settings: SyncSettings,
1162    ) -> Result<()> {
1163        let tracked = TrackedDatabase {
1164            database_id: database_id.into(),
1165            key_id: key_id.clone(),
1166            sync_settings,
1167        };
1168        // Single transaction for all operations
1169        let tx = self.user_database.new_transaction().await?;
1170        let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
1171
1172        // Use database ID as the key - check if it already exists (O(1))
1173        let db_id_key = tracked.database_id.to_string();
1174        let existing = databases_table.get(&db_id_key).await.ok();
1175
1176        // Determine if we need to validate and setup key mapping
1177        let needs_key_validation = match &existing {
1178            Some(existing) => existing.key_id != tracked.key_id, // Key changed
1179            None => true,                                        // New database
1180        };
1181
1182        // Validate key and set up mapping if needed
1183        if needs_key_validation {
1184            self.validate_and_map_key_in_txn(&tx, &tracked.database_id, &tracked.key_id)
1185                .await?;
1186        }
1187
1188        // Store using database ID as explicit key (not using insert's auto-generated UUID)
1189        databases_table.set(&db_id_key, tracked).await?;
1190
1191        // Single commit for all changes
1192        tx.commit().await?;
1193
1194        // Update sync system to immediately recompute combined settings
1195        // This ensures automatic sync works right away, without waiting for background worker
1196        if let Some(sync) = self.instance.sync() {
1197            // Auto-sync user tracking if not already synced
1198            // This is idempotent - safe to call multiple times
1199            sync.sync_user(&self.user_uuid, self.user_database.root_id())
1200                .await?;
1201        }
1202
1203        Ok(())
1204    }
1205
1206    /// List all tracked databases.
1207    ///
1208    /// Returns all databases this user has added to their tracked list.
1209    ///
1210    /// # Returns
1211    /// Vector of TrackedDatabase entries
1212    pub async fn databases(&self) -> Result<Vec<TrackedDatabase>> {
1213        let databases_table = self
1214            .user_database
1215            .get_store_viewer::<Table<TrackedDatabase>>("databases")
1216            .await?;
1217
1218        // Get all entries from the table (returns Vec<(key, value)>)
1219        let all_entries = databases_table.search(|_| true).await?;
1220
1221        // Extract just the values
1222        let tracked: Vec<TrackedDatabase> = all_entries.into_iter().map(|(_key, db)| db).collect();
1223
1224        Ok(tracked)
1225    }
1226
1227    /// Get a specific tracked database by ID.
1228    ///
1229    /// # Arguments
1230    /// * `database_id` - The ID of the database
1231    ///
1232    /// # Returns
1233    /// The TrackedDatabase if it's in the user's tracked list
1234    ///
1235    /// # Errors
1236    /// Returns `DatabaseNotTracked` if the database is not in the user's list
1237    pub async fn database(&self, database_id: &ID) -> Result<TrackedDatabase> {
1238        let databases_table = self
1239            .user_database()
1240            .get_store_viewer::<Table<TrackedDatabase>>("databases")
1241            .await?;
1242
1243        // Direct O(1) lookup using database ID as key
1244        let db_id_key = database_id.to_string();
1245        databases_table.get(&db_id_key).await.map_err(|_| {
1246            UserError::DatabaseNotTracked {
1247                database_id: database_id.clone(),
1248            }
1249            .into()
1250        })
1251    }
1252
1253    /// Enable sync for a tracked database.
1254    ///
1255    /// Sets `sync_enabled = true` on the user's preference for this database,
1256    /// preserving `sync_on_commit`, `interval_seconds`, and `properties`.
1257    /// Propagates the change to the host-level combined sync state by
1258    /// calling [`Sync::sync_user`] if sync is attached to the instance.
1259    ///
1260    /// No-op if already enabled.
1261    ///
1262    /// # Errors
1263    /// Returns `DatabaseNotTracked` if the database is not in the user's list.
1264    pub async fn enable_sync(&mut self, database_id: &ID) -> Result<()> {
1265        self.set_sync_enabled(database_id, true).await
1266    }
1267
1268    /// Disable sync for a tracked database.
1269    ///
1270    /// Sets `sync_enabled = false` on the user's preference for this database,
1271    /// preserving other sync settings. Propagates to the host via
1272    /// [`Sync::sync_user`] if sync is attached.
1273    ///
1274    /// The host-level combined state is OR'd across all users on the instance,
1275    /// so another user with sync enabled for the same database will keep the
1276    /// host serving it.
1277    ///
1278    /// No-op if already disabled.
1279    ///
1280    /// # Errors
1281    /// Returns `DatabaseNotTracked` if the database is not in the user's list.
1282    pub async fn disable_sync(&mut self, database_id: &ID) -> Result<()> {
1283        self.set_sync_enabled(database_id, false).await
1284    }
1285
1286    /// Enable sync for a tracked database and return a [`DatabaseTicket`]
1287    /// for handoff.
1288    ///
1289    /// Equivalent to building a ticket via
1290    /// [`Sync::create_ticket`](crate::sync::Sync::create_ticket) and then
1291    /// calling [`Self::enable_sync`], in a single call. The ticket carries the
1292    /// database ID plus any peer addresses that the instance's sync transports
1293    /// are currently advertising; a peer that imports the ticket via
1294    /// [`Sync::sync_with_ticket`](crate::sync::Sync::sync_with_ticket) will be
1295    /// able to fetch the database immediately.
1296    ///
1297    /// All preconditions (sync attached, transport registered) are checked
1298    /// before any user state is mutated, so a failed `share()` leaves the
1299    /// user's sync preference unchanged.
1300    ///
1301    /// # Errors
1302    /// - [`SyncError::SyncNotEnabled`] if sync is not attached to the instance
1303    ///   (call [`Instance::enable_sync`](crate::Instance::enable_sync) first).
1304    /// - [`SyncError::NoTransportEnabled`] if sync is attached but no transport
1305    ///   has been registered.
1306    /// - [`UserError::DatabaseNotTracked`] if the database is not in the user's
1307    ///   tracked list.
1308    pub async fn share(&mut self, database_id: &ID) -> Result<DatabaseTicket> {
1309        // Build the ticket first: this is the only step that can fail with
1310        // NoTransportEnabled / SyncNotEnabled, and it doesn't touch user state.
1311        // enable_sync runs last so any error path leaves preferences unchanged.
1312        let sync = self.instance.sync().ok_or(SyncError::SyncNotEnabled)?;
1313        let ticket = sync.create_ticket(database_id).await?;
1314        self.enable_sync(database_id).await?;
1315        Ok(ticket)
1316    }
1317
1318    /// Check whether this user has sync enabled for a tracked database.
1319    ///
1320    /// Returns this user's own preference, not the host's combined state.
1321    /// A `false` result means this user is not personally sharing the database;
1322    /// another user on the same instance may still have sync enabled for it.
1323    ///
1324    /// Returns `Ok(false)` for databases not tracked by this user.
1325    pub async fn is_sync_enabled(&self, database_id: &ID) -> Result<bool> {
1326        let databases_table = self
1327            .user_database()
1328            .get_store_viewer::<Table<TrackedDatabase>>("databases")
1329            .await?;
1330        let db_id_key = database_id.to_string();
1331        match databases_table.get(&db_id_key).await {
1332            Ok(tracked) => Ok(tracked.sync_settings.sync_enabled),
1333            Err(_) => Ok(false),
1334        }
1335    }
1336
1337    /// Internal helper backing [`Self::enable_sync`] and [`Self::disable_sync`].
1338    ///
1339    /// Reads the user's existing `TrackedDatabase`, updates only
1340    /// `sync_settings.sync_enabled`, and writes it back in a single transaction.
1341    /// All other fields on `TrackedDatabase` (`key_id`, `sync_on_commit`,
1342    /// `interval_seconds`, `properties`) are preserved as-is.
1343    ///
1344    /// Short-circuits without touching the user database when `sync_enabled`
1345    /// already matches `enabled`, so repeated calls don't create churn in the
1346    /// preferences DAG or trigger redundant `Sync::sync_user` propagation.
1347    ///
1348    /// On a real change, after the preferences commit succeeds, this calls
1349    /// [`Sync::sync_user`] when sync is attached to the instance so the
1350    /// host-level combined sync state (computed across all users by
1351    /// `merge_sync_settings`) updates immediately rather than waiting for
1352    /// the background worker.
1353    ///
1354    /// # Errors
1355    /// Returns `DatabaseNotTracked` if the database is not in the user's
1356    /// tracked list. The error is intentionally indistinguishable from a
1357    /// transaction read error on the tracking table — both are degenerate
1358    /// states from the caller's perspective.
1359    async fn set_sync_enabled(&mut self, database_id: &ID, enabled: bool) -> Result<()> {
1360        let tx = self.user_database.new_transaction().await?;
1361        let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
1362        let db_id_key = database_id.to_string();
1363
1364        let mut tracked =
1365            databases_table
1366                .get(&db_id_key)
1367                .await
1368                .map_err(|_| UserError::DatabaseNotTracked {
1369                    database_id: database_id.clone(),
1370                })?;
1371
1372        if tracked.sync_settings.sync_enabled == enabled {
1373            return Ok(());
1374        }
1375
1376        tracked.sync_settings.sync_enabled = enabled;
1377        databases_table.set(&db_id_key, tracked).await?;
1378        tx.commit().await?;
1379
1380        if let Some(sync) = self.instance.sync() {
1381            sync.sync_user(&self.user_uuid, self.user_database.root_id())
1382                .await?;
1383        }
1384
1385        Ok(())
1386    }
1387
1388    /// Stop tracking a database.
1389    ///
1390    /// This removes the database from the user's tracked list.
1391    /// It does not delete the database itself, remove key mappings, or delete any data.
1392    ///
1393    /// # Arguments
1394    /// * `database_id` - The ID of the database to stop tracking
1395    ///
1396    /// # Errors
1397    /// Returns `DatabaseNotTracked` if the database is not in the user's list
1398    pub async fn untrack_database(&mut self, database_id: &ID) -> Result<()> {
1399        let tx = self.user_database.new_transaction().await?;
1400        let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
1401
1402        // Direct O(1) delete using database ID as key
1403        let db_id_key = database_id.to_string();
1404
1405        // Verify it exists before deleting
1406        if databases_table.get(&db_id_key).await.is_err() {
1407            return Err(UserError::DatabaseNotTracked {
1408                database_id: database_id.clone(),
1409            }
1410            .into());
1411        }
1412
1413        // Delete using database ID as key
1414        databases_table.delete(&db_id_key).await?;
1415        tx.commit().await?;
1416
1417        Ok(())
1418    }
1419}