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