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        #[cfg(all(unix, feature = "service"))]
477        if let Some(conn) = self.instance.remote_connection() {
478            conn.register_session_key(signing_key).await?;
479            return Ok(Database::open_remote(&self.instance, conn, root_id, sigkey)
480                .await?
481                .with_key(key));
482        }
483        Ok(Database::open(&self.instance, root_id).await?.with_key(key))
484    }
485
486    /// Find databases by name among the user's tracked databases.
487    ///
488    /// Searches only the databases this user has tracked for those matching the given name.
489    ///
490    /// # Arguments
491    /// * `name` - Database name to search for
492    ///
493    /// # Returns
494    /// Vector of matching databases from the user's tracked list
495    pub async fn find_database(&self, name: impl AsRef<str>) -> Result<Vec<Database>> {
496        let name = name.as_ref();
497        let tracked = self.databases().await?;
498        let mut matching = Vec::new();
499
500        for tracked_db in tracked {
501            if let Ok(database) = self.open_database(&tracked_db.database_id).await
502                && let Ok(db_name) = database.get_name().await
503                && db_name == name
504            {
505                matching.push(database);
506            }
507        }
508
509        if matching.is_empty() {
510            Err(UserError::DatabaseNotFoundByName {
511                name: name.to_string(),
512            }
513            .into())
514        } else {
515            Ok(matching)
516        }
517    }
518
519    /// Find which key can access a database.
520    ///
521    /// Searches this user's keys to find one that can access the specified database.
522    /// Considers the SigKey mappings stored in user key metadata.
523    ///
524    /// Returns the key_id of a suitable key, preferring keys with mappings for this database.
525    ///
526    /// # Arguments
527    /// * `database_id` - The ID of the database
528    ///
529    /// # Returns
530    /// Some(key_id) if a suitable key is found, None if no keys can access this database
531    pub fn find_key(&self, database_id: &ID) -> Result<Option<PublicKey>> {
532        // Iterate through all keys and find ones with SigKey mappings for this database
533        for key_id in self.key_manager.list_key_ids() {
534            if let Some(metadata) = self.key_manager.get_key_metadata(&key_id)
535                && metadata.database_sigkeys.contains_key(database_id)
536            {
537                return Ok(Some(key_id));
538            }
539        }
540
541        // No key found with mapping for this database
542        Ok(None)
543    }
544
545    /// Get the resolved SigKey mapping for a key in a specific database.
546    ///
547    /// Users map their private keys to SigKey identifiers on a per-database basis.
548    /// This retrieves the resolved SigKey that a specific key uses in
549    /// a specific database's authentication settings.
550    ///
551    /// Internally, `None` in the stored mapping means "default pubkey identity",
552    /// which this method resolves to the concrete `SigKey::from_pubkey(...)` value.
553    ///
554    /// # Arguments
555    /// * `key_id` - The user's key identifier
556    /// * `database_id` - The database ID
557    ///
558    /// # Returns
559    /// `Ok(Some(sigkey))` if a mapping exists (resolved to concrete SigKey),
560    /// `Ok(None)` if no mapping is configured for this database
561    ///
562    /// # Errors
563    /// Returns an error if the key_id doesn't exist in the UserKeyManager
564    pub fn key_mapping(&self, key_id: &PublicKey, database_id: &ID) -> Result<Option<SigKey>> {
565        let metadata = self.key_manager.get_key_metadata(key_id).ok_or_else(|| {
566            super::errors::UserError::KeyNotFound {
567                key_id: key_id.to_string(),
568            }
569        })?;
570
571        match metadata.database_sigkeys.get(database_id) {
572            None => Ok(None), // no mapping exists
573            Some(None) => {
574                // Default: pubkey identity derived directly from key_id
575                Ok(Some(SigKey::from_pubkey(key_id)))
576            }
577            Some(Some(sigkey)) => Ok(Some(sigkey.clone())),
578        }
579    }
580
581    /// Map a key to a SigKey identity for a specific database.
582    ///
583    /// Registers that this user's key should be used with a specific SigKey identity
584    /// when interacting with a database. This is typically used when a user has been
585    /// granted access to a database and needs to configure their local key to work with it.
586    ///
587    /// If the provided SigKey matches the default pubkey identity for this key,
588    /// it is normalized to `None` internally (compact storage for the common case).
589    ///
590    /// # Multi-Key Support
591    ///
592    /// **Note**: A database may have mappings to multiple keys. This is useful for
593    /// multi-device scenarios where the same user wants to access a database from
594    /// different devices, each with their own key.
595    ///
596    /// # Arguments
597    /// * `key_id` - The user's key identifier (public key)
598    /// * `database_id` - The database ID
599    /// * `sigkey` - The SigKey identity to use for this database
600    ///
601    /// # Errors
602    /// Returns an error if the key_id doesn't exist in the user database
603    pub async fn map_key(
604        &mut self,
605        key_id: &PublicKey,
606        database_id: &ID,
607        sigkey: SigKey,
608    ) -> Result<()> {
609        let tx = self.user_database.new_transaction().await?;
610        self.map_key_in_txn(&tx, key_id, database_id, sigkey)
611            .await?;
612        tx.commit().await?;
613        Ok(())
614    }
615
616    /// Internal helper: Add a SigKey mapping within an existing transaction
617    ///
618    /// This is used internally by methods that manage their own transactions.
619    /// For external use, call `map_key()` instead.
620    ///
621    /// Normalizes the stored value: if the sigkey matches the default pubkey
622    /// identity for this key, stores `None` instead of `Some(sigkey)`.
623    async fn map_key_in_txn(
624        &mut self,
625        tx: &Transaction,
626        key_id: &PublicKey,
627        database_id: &ID,
628        sigkey: SigKey,
629    ) -> Result<()> {
630        use crate::store::Table;
631        use crate::user::types::UserKey;
632
633        let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
634
635        // Find the key metadata in the database
636        let (uuid_primary_key, mut metadata) = keys_table
637            .search(|uk| &uk.key_id == key_id)
638            .await?
639            .into_iter()
640            .next()
641            .ok_or_else(|| super::errors::UserError::KeyNotFound {
642                key_id: key_id.to_string(),
643            })?;
644
645        // Normalize: if the sigkey matches the default pubkey identity, store None
646        let default_sigkey = SigKey::from_pubkey(key_id);
647        let stored = if sigkey == default_sigkey {
648            None
649        } else {
650            Some(sigkey)
651        };
652
653        // Add the database sigkey mapping
654        metadata
655            .database_sigkeys
656            .insert(database_id.clone(), stored);
657
658        // Update the key in user database using the UUID primary key
659        keys_table.set(&uuid_primary_key, metadata.clone()).await?;
660
661        // Update the in-memory key manager with the updated metadata
662        self.key_manager.add_key(metadata)?;
663
664        Ok(())
665    }
666
667    /// Internal helper: Validate key and set up SigKey mapping within an existing transaction
668    ///
669    /// This validates that a key exists and has access to a database, discovers the appropriate
670    /// SigKey, and creates the mapping. Used by track_database (which has upsert behavior).
671    async fn validate_and_map_key_in_txn(
672        &mut self,
673        tx: &Transaction,
674        database_id: &ID,
675        key_id: &PublicKey,
676    ) -> Result<()> {
677        // Verify the key exists
678        if self.key_manager.get_signing_key(key_id).is_none() {
679            return Err(UserError::KeyNotFound {
680                key_id: key_id.to_string(),
681            }
682            .into());
683        }
684
685        // Discover available SigKeys for this public key
686        let available_sigkeys = Database::find_sigkeys(&self.instance, database_id, key_id).await?;
687
688        if available_sigkeys.is_empty() {
689            return Err(UserError::NoSigKeyFound {
690                key_id: key_id.to_string(),
691                database_id: database_id.clone(),
692            }
693            .into());
694        }
695
696        // Select the first SigKey (highest permission, since find_sigkeys returns sorted list)
697        let (sigkey, _permission) = &available_sigkeys[0];
698
699        // Store the discovered SigKey directly (map_key_in_txn normalizes to None if default)
700        self.map_key_in_txn(tx, key_id, database_id, sigkey.clone())
701            .await?;
702
703        Ok(())
704    }
705
706    // === Key Management (User Context) ===
707
708    /// Add a new private key to this user's keyring.
709    ///
710    /// Generates a new Ed25519 keypair, encrypts it (for password-protected users)
711    /// or stores it unencrypted (for passwordless users), and adds it to the user's
712    /// key database.
713    ///
714    /// # Arguments
715    /// * `display_name` - Optional display name for the key
716    ///
717    /// # Returns
718    /// The key ID (public key string)
719    pub async fn add_private_key(&mut self, display_name: Option<&str>) -> Result<PublicKey> {
720        use crate::auth::crypto::generate_keypair;
721        use crate::store::Table;
722        use crate::user::types::{KeyStorage, UserKey};
723
724        // Generate new keypair
725        let (private_key, public_key) = generate_keypair();
726
727        // Get current timestamp using the instance's clock
728        let timestamp = self.instance.clock().now_secs();
729
730        // Prepare UserKey based on encryption type
731        let user_key = if let Some(encryption_key) = self.key_manager.encryption_key() {
732            // Password-protected user: encrypt the key
733            use crate::user::crypto::encrypt_private_key;
734            let (ciphertext, nonce) = encrypt_private_key(&private_key, encryption_key)?;
735
736            UserKey {
737                key_id: public_key.clone(),
738                storage: KeyStorage::Encrypted {
739                    algorithm: "aes-256-gcm".to_string(),
740                    ciphertext,
741                    nonce,
742                },
743                display_name: display_name.map(|s| s.to_string()),
744                created_at: timestamp,
745                last_used: None,
746                is_default: false, // New keys are not default
747                database_sigkeys: HashMap::new(),
748            }
749        } else {
750            // Passwordless user: store unencrypted
751            UserKey {
752                key_id: public_key.clone(),
753                storage: KeyStorage::Unencrypted { key: private_key },
754                display_name: display_name.map(|s| s.to_string()),
755                created_at: timestamp,
756                last_used: None,
757                is_default: false, // New keys are not default
758                database_sigkeys: HashMap::new(),
759            }
760        };
761
762        // Store in user database
763        let tx = self.user_database.new_transaction().await?;
764        let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
765        keys_table.insert(user_key.clone()).await?;
766        tx.commit().await?;
767
768        // Add to in-memory key manager
769        self.key_manager.add_key(user_key)?;
770
771        Ok(public_key)
772    }
773
774    /// List all key IDs owned by this user.
775    ///
776    /// Keys are returned sorted by creation timestamp (oldest first), making the
777    /// first key in the list the "default" key created when the user was set up.
778    ///
779    /// # Returns
780    /// Vector of PublicKeys sorted by creation time
781    pub fn list_keys(&self) -> Result<Vec<PublicKey>> {
782        Ok(self.key_manager.list_key_ids())
783    }
784
785    /// Get the default key.
786    ///
787    /// Returns the key marked as is_default=true, or falls back to the oldest key
788    /// by creation timestamp if no default is explicitly set.
789    ///
790    /// # Returns
791    /// The PublicKey of the default key
792    ///
793    /// # Errors
794    /// Returns an error if no keys exist
795    pub fn get_default_key(&self) -> Result<PublicKey> {
796        self.key_manager
797            .get_default_key_id()
798            .ok_or_else(|| Error::from(InstanceError::AuthenticationRequired))
799    }
800
801    /// Get the display name set for a key.
802    ///
803    /// Display names are caller-supplied human-readable labels passed to
804    /// `add_private_key`. They have no cryptographic significance and are
805    /// not unique across keys. Returns `None` if the user does not hold a
806    /// key with this pubkey, or if it was added without a display name.
807    ///
808    /// # Arguments
809    /// * `key_id` - Public key of a key held by this user
810    pub fn key_display_name(&self, key_id: &PublicKey) -> Option<&str> {
811        self.key_manager
812            .get_key_metadata(key_id)
813            .and_then(|metadata| metadata.display_name.as_deref())
814    }
815
816    /// Find user-held keys whose display name matches `name`.
817    ///
818    /// Display names are not unique, so this returns every key whose
819    /// `display_name` exactly matches the provided string. Keys without a
820    /// display name are never returned. Returns an empty `Vec` when there
821    /// are no matches.
822    ///
823    /// # Arguments
824    /// * `name` - Exact display name to match
825    pub fn find_keys_by_display_name(&self, name: &str) -> Vec<PublicKey> {
826        self.key_manager
827            .list_key_ids()
828            .into_iter()
829            .filter(|key_id| {
830                self.key_manager
831                    .get_key_metadata(key_id)
832                    .and_then(|metadata| metadata.display_name.as_deref())
833                    == Some(name)
834            })
835            .collect()
836    }
837
838    /// Get a signing key by its ID.
839    ///
840    /// # Arguments
841    /// * `key_id` - The public key identifier
842    ///
843    /// # Returns
844    /// The PrivateKey if found
845    #[cfg(any(test, feature = "testing"))]
846    pub fn get_signing_key(&self, key_id: &PublicKey) -> Result<crate::auth::crypto::PrivateKey> {
847        self.key_manager
848            .get_signing_key(key_id)
849            .cloned()
850            .ok_or_else(|| {
851                UserError::KeyNotFound {
852                    key_id: key_id.to_string(),
853                }
854                .into()
855            })
856    }
857
858    // === Bootstrap Request Management (User Context) ===
859
860    /// Get all pending bootstrap requests from the sync system.
861    ///
862    /// This is a convenience method that requires the Instance's Sync to be initialized.
863    ///
864    /// # Arguments
865    /// * `sync` - Reference to the Instance's Sync object
866    ///
867    /// # Returns
868    /// A vector of (request_id, bootstrap_request) pairs for pending requests
869    pub async fn pending_bootstrap_requests(
870        &self,
871        sync: &Sync,
872    ) -> Result<Vec<(String, BootstrapRequest)>> {
873        sync.pending_bootstrap_requests().await
874    }
875
876    /// Approve a bootstrap request and add the requesting key to the target database.
877    ///
878    /// The approving key must have Admin permission on the target database.
879    ///
880    /// # Arguments
881    /// * `sync` - Mutable reference to the Instance's Sync object
882    /// * `request_id` - The unique identifier of the request to approve
883    /// * `approving_key_id` - The ID of this user's key to use for approval (must have Admin permission)
884    ///
885    /// # Returns
886    /// Result indicating success or failure of the approval operation
887    ///
888    /// # Errors
889    /// - Returns an error if the user doesn't own the specified approving key
890    /// - Returns an error if the approving key doesn't have Admin permission on the target database
891    /// - Returns an error if the request doesn't exist or isn't pending
892    /// - Returns an error if the key addition to the database fails
893    pub async fn approve_bootstrap_request(
894        &self,
895        sync: &Sync,
896        request_id: &str,
897        approving_key_id: &PublicKey,
898    ) -> Result<()> {
899        // Get the signing key from the key manager
900        let signing_key = self
901            .key_manager
902            .get_signing_key(approving_key_id)
903            .ok_or_else(|| super::errors::UserError::KeyNotFound {
904                key_id: approving_key_id.to_string(),
905            })?;
906
907        // Delegate to Sync layer with the user-provided key
908        // The Sync layer will validate permissions when committing the transaction
909        let key = DatabaseKey::new(signing_key.clone());
910        sync.approve_bootstrap_request_with_key(request_id, &key)
911            .await?;
912
913        Ok(())
914    }
915
916    /// Reject a bootstrap request.
917    ///
918    /// This method marks the request as rejected. The requesting device will not
919    /// be granted access to the target database. Requires Admin permission on the
920    /// target database to prevent unauthorized users from disrupting the bootstrap protocol.
921    ///
922    /// # Arguments
923    /// * `sync` - Mutable reference to the Instance's Sync object
924    /// * `request_id` - The unique identifier of the request to reject
925    /// * `rejecting_key_id` - The ID of this user's key (for permission validation and audit trail)
926    ///
927    /// # Returns
928    /// Result indicating success or failure of the rejection operation
929    ///
930    /// # Errors
931    /// - Returns an error if the user doesn't own the specified rejecting key
932    /// - Returns an error if the request doesn't exist or isn't pending
933    /// - Returns an error if the rejecting key lacks Admin permission on the target database
934    pub async fn reject_bootstrap_request(
935        &self,
936        sync: &Sync,
937        request_id: &str,
938        rejecting_key_id: &PublicKey,
939    ) -> Result<()> {
940        // Get the signing key from the key manager
941        let signing_key = self
942            .key_manager
943            .get_signing_key(rejecting_key_id)
944            .ok_or_else(|| super::errors::UserError::KeyNotFound {
945                key_id: rejecting_key_id.to_string(),
946            })?;
947
948        // Delegate to Sync layer with the user-provided key
949        // The Sync layer will validate Admin permission on the target database
950        let key = DatabaseKey::new(signing_key.clone());
951        sync.reject_bootstrap_request_with_key(request_id, &key)
952            .await?;
953
954        Ok(())
955    }
956
957    /// Request access to a database from a peer (bootstrap sync).
958    ///
959    /// This convenience method initiates a bootstrap sync request to access a database
960    /// that this user doesn't have locally yet. The user's key will be sent to the peer
961    /// to request the specified permission level.
962    ///
963    /// This is useful for multi-device scenarios where a user wants to access their
964    /// existing database from a new device, or when requesting access to a database
965    /// shared by another user.
966    ///
967    /// # Arguments
968    /// * `sync` - Reference to the Instance's Sync object
969    /// * `ticket` - A ticket containing the database ID and address hints
970    /// * `key_id` - The ID of this user's key to use for the request
971    /// * `requested_permission` - The permission level being requested
972    ///
973    /// # Returns
974    /// Result indicating success or failure of the bootstrap request
975    ///
976    /// # Errors
977    /// - Returns an error if the user doesn't own the specified key
978    /// - Returns an error if all addresses in the ticket fail
979    /// - Returns an error if the bootstrap sync fails
980    ///
981    /// # Example
982    /// ```rust,ignore
983    /// // Request write access to a shared database
984    /// let user_key_id = user.get_default_key()?;
985    /// let ticket: DatabaseTicket = "eidetica:?db=sha256:abc...&pr=http:192.168.1.1:8080".parse()?;
986    /// user.request_database_access(
987    ///     &sync,
988    ///     &ticket,
989    ///     &user_key_id,
990    ///     Permission::Write(5),
991    /// ).await?;
992    ///
993    /// // After approval, the database can be opened
994    /// let database = user.open_database(ticket.database_id())?;
995    /// ```
996    pub async fn request_database_access(
997        &self,
998        sync: &Sync,
999        ticket: &DatabaseTicket,
1000        key_id: &PublicKey,
1001        requested_permission: Permission,
1002    ) -> Result<()> {
1003        if self.key_manager.get_signing_key(key_id).is_none() {
1004            return Err(super::errors::UserError::KeyNotFound {
1005                key_id: key_id.to_string(),
1006            }
1007            .into());
1008        }
1009
1010        let key_name = key_id.to_string();
1011
1012        sync.bootstrap_with_ticket(ticket, key_id, &key_name, requested_permission)
1013            .await
1014    }
1015
1016    // === Tracked Databases ===
1017
1018    /// Track a database, adding it to this user's list with auto-discovery of SigKeys.
1019    ///
1020    /// This method adds an existing database to your tracked list, or updates it if
1021    /// already tracked (upsert behavior).
1022    ///
1023    /// When tracking:
1024    /// 1. Uses Database::find_sigkeys() to discover which SigKey the user can use
1025    /// 2. Automatically selects the SigKey with highest permission
1026    /// 3. Stores the key mapping and sync settings
1027    ///
1028    /// The sync_settings indicate your sync preferences, but do not automatically
1029    /// configure sync. Use the Sync module's peer and tree methods to set up actual
1030    /// sync relationships.
1031    ///
1032    /// # Arguments
1033    /// * `database_id` - ID of the database to track
1034    /// * `key_id` - Which user key to use for this database
1035    /// * `sync_settings` - Sync preferences for this database
1036    ///
1037    /// # Returns
1038    /// Result indicating success or failure
1039    ///
1040    /// # Errors
1041    /// - Returns `NoSigKeyFound` if no SigKey can be found for the specified key
1042    /// - Returns `KeyNotFound` if the specified key_id doesn't exist
1043    pub async fn track_database(
1044        &mut self,
1045        database_id: impl Into<ID>,
1046        key_id: &PublicKey,
1047        sync_settings: SyncSettings,
1048    ) -> Result<()> {
1049        let tracked = TrackedDatabase {
1050            database_id: database_id.into(),
1051            key_id: key_id.clone(),
1052            sync_settings,
1053        };
1054        // Single transaction for all operations
1055        let tx = self.user_database.new_transaction().await?;
1056        let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
1057
1058        // Use database ID as the key - check if it already exists (O(1))
1059        let db_id_key = tracked.database_id.to_string();
1060        let existing = databases_table.get(&db_id_key).await.ok();
1061
1062        // Determine if we need to validate and setup key mapping
1063        let needs_key_validation = match &existing {
1064            Some(existing) => existing.key_id != tracked.key_id, // Key changed
1065            None => true,                                        // New database
1066        };
1067
1068        // Validate key and set up mapping if needed
1069        if needs_key_validation {
1070            self.validate_and_map_key_in_txn(&tx, &tracked.database_id, &tracked.key_id)
1071                .await?;
1072        }
1073
1074        // Store using database ID as explicit key (not using insert's auto-generated UUID)
1075        databases_table.set(&db_id_key, tracked).await?;
1076
1077        // Single commit for all changes
1078        tx.commit().await?;
1079
1080        // Update sync system to immediately recompute combined settings
1081        // This ensures automatic sync works right away, without waiting for background worker
1082        if let Some(sync) = self.instance.sync() {
1083            // Auto-sync user tracking if not already synced
1084            // This is idempotent - safe to call multiple times
1085            sync.sync_user(&self.user_uuid, self.user_database.root_id())
1086                .await?;
1087        }
1088
1089        Ok(())
1090    }
1091
1092    /// List all tracked databases.
1093    ///
1094    /// Returns all databases this user has added to their tracked list.
1095    ///
1096    /// # Returns
1097    /// Vector of TrackedDatabase entries
1098    pub async fn databases(&self) -> Result<Vec<TrackedDatabase>> {
1099        let databases_table = self
1100            .user_database
1101            .get_store_viewer::<Table<TrackedDatabase>>("databases")
1102            .await?;
1103
1104        // Get all entries from the table (returns Vec<(key, value)>)
1105        let all_entries = databases_table.search(|_| true).await?;
1106
1107        // Extract just the values
1108        let tracked: Vec<TrackedDatabase> = all_entries.into_iter().map(|(_key, db)| db).collect();
1109
1110        Ok(tracked)
1111    }
1112
1113    /// Get a specific tracked database by ID.
1114    ///
1115    /// # Arguments
1116    /// * `database_id` - The ID of the database
1117    ///
1118    /// # Returns
1119    /// The TrackedDatabase if it's in the user's tracked list
1120    ///
1121    /// # Errors
1122    /// Returns `DatabaseNotTracked` if the database is not in the user's list
1123    pub async fn database(&self, database_id: &ID) -> Result<TrackedDatabase> {
1124        let databases_table = self
1125            .user_database()
1126            .get_store_viewer::<Table<TrackedDatabase>>("databases")
1127            .await?;
1128
1129        // Direct O(1) lookup using database ID as key
1130        let db_id_key = database_id.to_string();
1131        databases_table.get(&db_id_key).await.map_err(|_| {
1132            UserError::DatabaseNotTracked {
1133                database_id: database_id.clone(),
1134            }
1135            .into()
1136        })
1137    }
1138
1139    /// Enable sync for a tracked database.
1140    ///
1141    /// Sets `sync_enabled = true` on the user's preference for this database,
1142    /// preserving `sync_on_commit`, `interval_seconds`, and `properties`.
1143    /// Propagates the change to the host-level combined sync state by
1144    /// calling [`Sync::sync_user`] if sync is attached to the instance.
1145    ///
1146    /// No-op if already enabled.
1147    ///
1148    /// # Errors
1149    /// Returns `DatabaseNotTracked` if the database is not in the user's list.
1150    pub async fn enable_sync(&mut self, database_id: &ID) -> Result<()> {
1151        self.set_sync_enabled(database_id, true).await
1152    }
1153
1154    /// Disable sync for a tracked database.
1155    ///
1156    /// Sets `sync_enabled = false` on the user's preference for this database,
1157    /// preserving other sync settings. Propagates to the host via
1158    /// [`Sync::sync_user`] if sync is attached.
1159    ///
1160    /// The host-level combined state is OR'd across all users on the instance,
1161    /// so another user with sync enabled for the same database will keep the
1162    /// host serving it.
1163    ///
1164    /// No-op if already disabled.
1165    ///
1166    /// # Errors
1167    /// Returns `DatabaseNotTracked` if the database is not in the user's list.
1168    pub async fn disable_sync(&mut self, database_id: &ID) -> Result<()> {
1169        self.set_sync_enabled(database_id, false).await
1170    }
1171
1172    /// Enable sync for a tracked database and return a [`DatabaseTicket`]
1173    /// for handoff.
1174    ///
1175    /// Equivalent to building a ticket via
1176    /// [`Sync::create_ticket`](crate::sync::Sync::create_ticket) and then
1177    /// calling [`Self::enable_sync`], in a single call. The ticket carries the
1178    /// database ID plus any peer addresses that the instance's sync transports
1179    /// are currently advertising; a peer that imports the ticket via
1180    /// [`Sync::sync_with_ticket`](crate::sync::Sync::sync_with_ticket) will be
1181    /// able to fetch the database immediately.
1182    ///
1183    /// All preconditions (sync attached, transport registered) are checked
1184    /// before any user state is mutated, so a failed `share()` leaves the
1185    /// user's sync preference unchanged.
1186    ///
1187    /// # Errors
1188    /// - [`SyncError::SyncNotEnabled`] if sync is not attached to the instance
1189    ///   (call [`Instance::enable_sync`](crate::Instance::enable_sync) first).
1190    /// - [`SyncError::NoTransportEnabled`] if sync is attached but no transport
1191    ///   has been registered.
1192    /// - [`UserError::DatabaseNotTracked`] if the database is not in the user's
1193    ///   tracked list.
1194    pub async fn share(&mut self, database_id: &ID) -> Result<DatabaseTicket> {
1195        // Build the ticket first: this is the only step that can fail with
1196        // NoTransportEnabled / SyncNotEnabled, and it doesn't touch user state.
1197        // enable_sync runs last so any error path leaves preferences unchanged.
1198        let sync = self.instance.sync().ok_or(SyncError::SyncNotEnabled)?;
1199        let ticket = sync.create_ticket(database_id).await?;
1200        self.enable_sync(database_id).await?;
1201        Ok(ticket)
1202    }
1203
1204    /// Check whether this user has sync enabled for a tracked database.
1205    ///
1206    /// Returns this user's own preference, not the host's combined state.
1207    /// A `false` result means this user is not personally sharing the database;
1208    /// another user on the same instance may still have sync enabled for it.
1209    ///
1210    /// Returns `Ok(false)` for databases not tracked by this user.
1211    pub async fn is_sync_enabled(&self, database_id: &ID) -> Result<bool> {
1212        let databases_table = self
1213            .user_database()
1214            .get_store_viewer::<Table<TrackedDatabase>>("databases")
1215            .await?;
1216        let db_id_key = database_id.to_string();
1217        match databases_table.get(&db_id_key).await {
1218            Ok(tracked) => Ok(tracked.sync_settings.sync_enabled),
1219            Err(_) => Ok(false),
1220        }
1221    }
1222
1223    /// Internal helper backing [`Self::enable_sync`] and [`Self::disable_sync`].
1224    ///
1225    /// Reads the user's existing `TrackedDatabase`, updates only
1226    /// `sync_settings.sync_enabled`, and writes it back in a single transaction.
1227    /// All other fields on `TrackedDatabase` (`key_id`, `sync_on_commit`,
1228    /// `interval_seconds`, `properties`) are preserved as-is.
1229    ///
1230    /// Short-circuits without touching the user database when `sync_enabled`
1231    /// already matches `enabled`, so repeated calls don't create churn in the
1232    /// preferences DAG or trigger redundant `Sync::sync_user` propagation.
1233    ///
1234    /// On a real change, after the preferences commit succeeds, this calls
1235    /// [`Sync::sync_user`] when sync is attached to the instance so the
1236    /// host-level combined sync state (computed across all users by
1237    /// `merge_sync_settings`) updates immediately rather than waiting for
1238    /// the background worker.
1239    ///
1240    /// # Errors
1241    /// Returns `DatabaseNotTracked` if the database is not in the user's
1242    /// tracked list. The error is intentionally indistinguishable from a
1243    /// transaction read error on the tracking table — both are degenerate
1244    /// states from the caller's perspective.
1245    async fn set_sync_enabled(&mut self, database_id: &ID, enabled: bool) -> Result<()> {
1246        let tx = self.user_database.new_transaction().await?;
1247        let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
1248        let db_id_key = database_id.to_string();
1249
1250        let mut tracked =
1251            databases_table
1252                .get(&db_id_key)
1253                .await
1254                .map_err(|_| UserError::DatabaseNotTracked {
1255                    database_id: database_id.clone(),
1256                })?;
1257
1258        if tracked.sync_settings.sync_enabled == enabled {
1259            return Ok(());
1260        }
1261
1262        tracked.sync_settings.sync_enabled = enabled;
1263        databases_table.set(&db_id_key, tracked).await?;
1264        tx.commit().await?;
1265
1266        if let Some(sync) = self.instance.sync() {
1267            sync.sync_user(&self.user_uuid, self.user_database.root_id())
1268                .await?;
1269        }
1270
1271        Ok(())
1272    }
1273
1274    /// Stop tracking a database.
1275    ///
1276    /// This removes the database from the user's tracked list.
1277    /// It does not delete the database itself, remove key mappings, or delete any data.
1278    ///
1279    /// # Arguments
1280    /// * `database_id` - The ID of the database to stop tracking
1281    ///
1282    /// # Errors
1283    /// Returns `DatabaseNotTracked` if the database is not in the user's list
1284    pub async fn untrack_database(&mut self, database_id: &ID) -> Result<()> {
1285        let tx = self.user_database.new_transaction().await?;
1286        let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
1287
1288        // Direct O(1) delete using database ID as key
1289        let db_id_key = database_id.to_string();
1290
1291        // Verify it exists before deleting
1292        if databases_table.get(&db_id_key).await.is_err() {
1293            return Err(UserError::DatabaseNotTracked {
1294                database_id: database_id.clone(),
1295            }
1296            .into());
1297        }
1298
1299        // Delete using database ID as key
1300        databases_table.delete(&db_id_key).await?;
1301        tx.commit().await?;
1302
1303        Ok(())
1304    }
1305}