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