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}