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

eidetica/account/session/
mod.rs

1//! AccountSession management
2//!
3//! Represents an authenticated account session with decrypted keys.
4//!
5//! # API Overview
6//!
7//! The AccountSession 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 account key
14//! - **`join()`** - Join a database from a ticket and open it
15//! - **`find_database()`** - Search for databases by name
16//!
17//! ## Application References
18//!
19//! - **`set_reference()`** - Store a direct database reference in the Account
20//! - **`reference()`** - Read a reference (`None` only when absent)
21//! - **`open_reference()`** - Read a reference and open its database
22//!
23//! ## Tracked Databases
24//!
25//! Manage your personal list of tracked databases:
26//!
27//! - **`databases()`** - List all tracked databases
28//! - **`database()`** - Get a specific tracked database
29//! - **`tracked_database()`** - Read one tracking row, distinguishing absence from failure
30//! - **`track_database()`** - Add or update a tracked database (upsert)
31//! - **`set_sync_preferences()`** - Update preferences without changing tracking or key mappings
32//! - **`untrack_database()`** - Remove a database from your tracked list
33//! - **`is_sync_enabled()`** - Check this account's sync preference for a database
34//!
35//! ## Key-Database Mappings
36//!
37//! Control which keys access which databases:
38//!
39//! - **`map_key()`** - Map a key to a SigKey identifier for a database
40//! - **`key_mapping()`** - Get the SigKey mapping for a key-database pair
41//! - **`find_key()`** - Find which key can access a database
42//!
43//! This explicit approach ensures predictable behavior and avoids ambiguity about which
44//! keys have access to which databases.
45
46use std::collections::HashMap;
47
48use std::sync::Arc;
49
50use super::{Account, AccountKeyManager, admin::InstanceAdmin, types::AccountInfo};
51use crate::{
52    Database, Error, Instance, Result, Transaction,
53    account::{AccountError, SyncSettings, TrackedDatabase},
54    auth::{Permission, SigKey, crypto::PublicKey},
55    crdt::Doc,
56    database::DatabaseKey,
57    entry::ID,
58    instance::{InstanceError, backend::Backend},
59    store::{DocStore, StoreError, Table},
60    sync::{BootstrapRequest, DatabaseTicket, Sync, SyncError},
61};
62
63mod builder;
64#[cfg(test)]
65mod tests;
66
67pub use builder::DatabaseBuilder;
68
69/// Unlocked Account access, created by login or explicit caller-held credentials
70///
71/// Represents an authenticated account with decrypted private keys loaded in memory.
72/// The AccountSession struct provides access to key management, tracked databases, and
73/// bootstrap approval operations.
74pub struct AccountSession {
75    /// Stable internal account UUID (Table primary key)
76    login_id: String,
77
78    /// Username (login identifier)
79    username: String,
80
81    /// The Account, encapsulating its private references and public Auth
82    account: Account,
83
84    /// Instance reference for database operations
85    instance: Instance,
86
87    /// Decrypted account keys (in memory only during session)
88    key_manager: AccountKeyManager,
89
90    /// Account info (cached from the native login record)
91    account_info: Option<AccountInfo>,
92
93    /// Caller-held sessions have no daemon registration; their bookkeeping is
94    /// session-local and never sent to the service credential store.
95    caller_state: Option<tokio::sync::Mutex<super::local_state::LocalAccountState>>,
96}
97
98impl std::fmt::Debug for AccountSession {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        f.debug_struct("AccountSession")
101            .field("login_id", &self.login_id)
102            .field("username", &self.username)
103            .field("account", &self.account)
104            .field("instance", &self.instance)
105            .field("key_manager", &"<KeyManager [sensitive]>")
106            .field("account_info", &self.account_info)
107            .finish()
108    }
109}
110
111impl AccountSession {
112    /// Create a new AccountSession
113    ///
114    /// This is an internal constructor used after successful login.
115    /// Use `Instance::login_account()` to create an AccountSession.
116    ///
117    /// # Arguments
118    /// * `login_id` - Internal UUID (Table primary key)
119    /// * `account_info` - Account information from the native login record
120    /// * `account_database` - The account's private database
121    /// * `instance` - Instance reference
122    /// * `key_manager` - Initialized key manager with decrypted keys
123    #[allow(dead_code)]
124    pub(crate) fn new(
125        login_id: String,
126        account_info: AccountInfo,
127        account_database: Database,
128        instance: Instance,
129        key_manager: AccountKeyManager,
130    ) -> Self {
131        Self {
132            login_id,
133            username: account_info.username.clone(),
134            account: Account::new(account_database),
135            instance,
136            key_manager,
137            account_info: Some(account_info),
138            caller_state: None,
139        }
140    }
141
142    /// Open a full session using an explicit signer and Account locator, without
143    /// reading or registering a daemon login. Ordinary Account Auth is checked.
144    /// Key hints, activation and tracking live only for this session; background
145    /// sync is not enabled. The signer may use an explicit delegation identity.
146    /// For a service, supply a connection authenticated by [`super::AccountSigner::connect`];
147    /// this constructor never reauthenticates or rebinds an existing connection.
148    pub async fn from_signer(instance: &Instance, signer: super::AccountSigner) -> Result<Self> {
149        let credentials = super::UnlockedCredentials::from_signer(
150            signer.account_id().clone(),
151            vec![signer.signing_key().clone()],
152        )?;
153        Self::from_unlocked(instance, signer, credentials).await
154    }
155
156    /// Consume caller-unlocked credentials using an explicitly selected Account
157    /// key. No personal/default key fallback or daemon bundle lookup occurs.
158    /// Persisting caller-owned credentials remains the caller's responsibility;
159    /// session additions do not alter the original encrypted bundle.
160    pub async fn from_credentials(
161        instance: &Instance,
162        credentials: super::UnlockedCredentials,
163        account_key: &PublicKey,
164    ) -> Result<Self> {
165        let signer = super::AccountSigner::new(
166            credentials.account_id().clone(),
167            credentials.signer(account_key)?,
168        );
169        Self::from_unlocked(instance, signer, credentials).await
170    }
171
172    async fn from_unlocked(
173        instance: &Instance,
174        signer: super::AccountSigner,
175        credentials: super::UnlockedCredentials,
176    ) -> Result<Self> {
177        let account = signer.open_account(instance).await?;
178        let keys: std::collections::BTreeMap<_, _> = credentials
179            .key_ids()
180            .into_iter()
181            .map(|id| {
182                let is_default = id == signer.key_id();
183                let metadata = super::AccountKey {
184                    key_id: id.clone(),
185                    display_name: None,
186                    created_at: instance.clock().now_secs(),
187                    last_used: None,
188                    is_default,
189                    database_sigkeys: if is_default {
190                        HashMap::from([(account.id().clone(), Some(signer.identity().clone()))])
191                    } else {
192                        HashMap::new()
193                    },
194                };
195                (id.to_string(), metadata)
196            })
197            .collect();
198        let state = super::local_state::LocalAccountState {
199            keys: keys.clone(),
200            ..Default::default()
201        };
202        let key_manager = AccountKeyManager::new(credentials, keys.into_values().collect())?;
203        Ok(Self {
204            login_id: String::new(),
205            username: String::new(),
206            account,
207            instance: instance.clone(),
208            key_manager,
209            account_info: None,
210            caller_state: Some(tokio::sync::Mutex::new(state)),
211        })
212    }
213
214    async fn read_local_state(&self) -> Result<super::local_state::LocalAccountState> {
215        match &self.caller_state {
216            Some(state) => Ok(state.lock().await.clone()),
217            None => super::local_state::read(&self.instance, &self.login_id).await,
218        }
219    }
220
221    async fn update_local_state(
222        &self,
223        edit: impl FnMut(&mut super::local_state::LocalAccountState) -> Result<()>,
224    ) -> Result<super::local_state::LocalAccountState> {
225        if let Some(state) = &self.caller_state {
226            let mut state = state.lock().await;
227            let mut next = state.clone();
228            let mut edit = edit;
229            edit(&mut next)?;
230            *state = next.clone();
231            Ok(next)
232        } else {
233            super::local_state::update(&self.instance, &self.login_id, edit).await
234        }
235    }
236
237    async fn refresh_local_sync(&self) -> Result<()> {
238        if self.caller_state.is_none() {
239            super::local_state::refresh_sync(&self.instance, &self.login_id, self.account.id())
240                .await?;
241        }
242        Ok(())
243    }
244
245    fn with_local_account(&self, database: Database) -> Database {
246        if self.caller_state.is_some() {
247            database
248        } else {
249            database.with_local_account(&self.login_id, self.account.id())
250        }
251    }
252
253    /// Bind a newly selected actual signer without changing existing handles.
254    /// `None` selects direct-key Auth; delegation requires an explicit identity.
255    /// References and key-to-Identity hints never supply authority here.
256    pub fn acting_as(
257        &self,
258        key_id: &PublicKey,
259        identity: Option<SigKey>,
260    ) -> Result<super::AccountSigner> {
261        let signer = self
262            .key_manager
263            .get_signing_key(key_id)
264            .cloned()
265            .ok_or_else(|| AccountError::KeyNotFound {
266                key_id: key_id.to_string(),
267            })?;
268        let key = match identity {
269            Some(identity) => DatabaseKey::with_identity(signer, identity),
270            None => DatabaseKey::new(signer),
271        };
272        Ok(super::AccountSigner::new(self.account.id().clone(), key))
273    }
274
275    // === Basic Session Methods ===
276
277    /// Instance-local login record UUID, or empty for a caller-held session.
278    /// This is not the Account's portable identity.
279    /// Use [`Account::id`] (via [`Self::account`]) for the Account's identity.
280    pub fn login_id(&self) -> &str {
281        &self.login_id
282    }
283
284    /// Local login name, or empty for a caller-held session.
285    pub fn username(&self) -> &str {
286        &self.username
287    }
288
289    /// The Account this session unlocks.
290    ///
291    /// Its root ID is the Account's stable identity. Read and open application
292    /// references with [`Self::reference`] and [`Self::open_reference`].
293    pub fn account(&self) -> &Account {
294        &self.account
295    }
296
297    /// Get a reference to the backend seam.
298    pub fn backend(&self) -> &Arc<dyn Backend> {
299        self.instance.backend()
300    }
301
302    /// Registered login metadata, or `None` for a caller-held session.
303    pub fn account_info(&self) -> Option<&AccountInfo> {
304        self.account_info.as_ref()
305    }
306
307    /// Whether this account is an instance admin.
308    ///
309    /// "Instance admin" means the account's default pubkey holds `Admin` in the
310    /// `_users` system database's `auth_settings`. The `admin`/`admin` account
311    /// created during instance bootstrap is auto-promoted; subsequent accounts
312    /// land as non-admins until promoted via
313    /// [`InstanceAdmin::grant_instance_admin`](crate::account::InstanceAdmin::grant_instance_admin).
314    ///
315    /// Returns `false` if the key resolution fails (key not in auth_settings,
316    /// non-Admin permission, etc.). For the capability handle rather than a
317    /// bool, use [`Self::admin`].
318    pub async fn is_admin(&self) -> Result<bool> {
319        self.admin_check().await
320    }
321
322    /// Obtain the instance-admin capability view.
323    ///
324    /// Returns an [`InstanceAdmin`] only if this account's default key holds
325    /// `Admin` on the `_users` system database. All admin-gated operations
326    /// (creating accounts, listing accounts, promoting admins) live on the
327    /// returned view, so the privilege boundary is explicit at the call site
328    /// and those operations need no further permission check of their own.
329    ///
330    /// # Errors
331    /// - [`AccountError::InsufficientPermissions`] if this account is not an
332    ///   instance admin.
333    pub async fn admin(&self) -> Result<InstanceAdmin<'_>> {
334        if self.admin_check().await? {
335            Ok(InstanceAdmin::new(self))
336        } else {
337            Err(AccountError::InsufficientPermissions.into())
338        }
339    }
340
341    /// Single source of truth for "is this account an instance admin".
342    ///
343    /// Reads the `_users` `auth_settings` snapshot via the account's **session
344    /// key** (not the device key), so it behaves identically on local and
345    /// remote instances. Returns `Ok(false)` for a non-admin or unresolved
346    /// key; propagates real infrastructure errors. Shared by
347    /// [`Self::is_admin`] and [`Self::admin`].
348    async fn admin_check(&self) -> Result<bool> {
349        let default_pubkey =
350            self.key_manager
351                .get_default_key_id()
352                .ok_or_else(|| AccountError::KeyNotFound {
353                    key_id: "<default>".to_string(),
354                })?;
355        let signing_key = self.default_signing_key()?;
356        let users_db = self.instance.users_db_for_session(&signing_key).await?;
357        let tx = users_db.new_transaction().await?;
358        let settings = tx.get_settings()?;
359        let auth = settings.auth_snapshot().await?;
360        match auth.get_key_by_pubkey(&default_pubkey) {
361            Ok(key) => Ok(matches!(key.permissions(), Permission::Admin(_))),
362            Err(_) => Ok(false),
363        }
364    }
365
366    /// The account's default signing key (decrypted, in-session).
367    ///
368    /// Shared by admin / system-DB paths that must sign as the account
369    /// directly, rather than via a tracked-database SigKey mapping.
370    pub(crate) fn default_signing_key(&self) -> Result<crate::auth::crypto::PrivateKey> {
371        let key_id =
372            self.key_manager
373                .get_default_key_id()
374                .ok_or_else(|| AccountError::KeyNotFound {
375                    key_id: "<default>".to_string(),
376                })?;
377        self.key_manager
378            .get_signing_key(&key_id)
379            .cloned()
380            .ok_or_else(|| {
381                AccountError::KeyNotFound {
382                    key_id: key_id.to_string(),
383                }
384                .into()
385            })
386    }
387
388    /// Instance reference (internal — for admin / system-DB helpers).
389    pub(crate) fn instance(&self) -> &Instance {
390        &self.instance
391    }
392
393    /// Logout (consumes self and clears decrypted keys from memory)
394    ///
395    /// The session's key manager is cleared on drop. Independently retained
396    /// signer/database handles own their own key clones and remain usable;
397    /// logout is not revocation or invalidation of those handles.
398    pub fn logout(self) -> Result<()> {
399        // Consume self, all keys are stored in other Types that zeroize themselves on drop
400        Ok(())
401    }
402
403    // === Key Manager Access (Internal) ===
404
405    /// Get a reference to the key manager (for internal use)
406    #[allow(dead_code)]
407    pub(crate) fn key_manager(&self) -> &AccountKeyManager {
408        &self.key_manager
409    }
410
411    /// Get a mutable reference to the key manager (for internal use)
412    #[allow(dead_code)]
413    pub(crate) fn key_manager_mut(&mut self) -> &mut AccountKeyManager {
414        &mut self.key_manager
415    }
416
417    // === Database Operations (AccountSession Context) ===
418
419    /// Start building a new database via the chainable [`DatabaseBuilder`] API.
420    ///
421    /// The builder collects settings, key policy, and store initializers, then
422    /// produces a fully-initialized database in a single genesis entry when
423    /// [`DatabaseBuilder::build`] is called.
424    ///
425    /// For the lower-level direct constructor see [`Self::create_database`].
426    pub fn new_database(&mut self) -> DatabaseBuilder<'_> {
427        DatabaseBuilder::new(self)
428    }
429
430    /// Create a new database with explicit key selection.
431    ///
432    /// This method requires you to specify which key should be used to create and manage
433    /// the database, providing explicit control over key-database relationships.
434    ///
435    /// # Arguments
436    /// * `settings` - Initial database settings (metadata, name, etc.)
437    /// * `key_id` - The ID of the key to use for this database (public key string)
438    ///
439    /// # Returns
440    /// The created Database
441    ///
442    /// # Errors
443    /// - Returns an error if the specified key_id doesn't exist
444    /// - Returns an error if the key cannot be retrieved
445    ///
446    /// # Example
447    /// ```rust,ignore
448    /// // Get available keys
449    /// let keys = account.list_keys()?;
450    /// let key_id = &keys[1]; // Use the second key
451    ///
452    /// // Create database with explicit key selection
453    /// let mut settings = Doc::new();
454    /// settings.set("name", "My Database");
455    /// let database = account.new_database(settings, key_id)?;
456    /// ```
457    pub async fn create_database(&mut self, settings: Doc, key_id: &PublicKey) -> Result<Database> {
458        self.create_database_with_init(settings, key_id, async |_| Ok(()))
459            .await
460    }
461
462    /// Creates a new database with an initialization callback that runs inside
463    /// the genesis transaction.
464    ///
465    /// This is the underlying constructor used by [`Self::create_database`] and by
466    /// [`Self::new_database`] (the builder API). The callback receives the
467    /// genesis transaction after `_settings` and `_root` have been staged but
468    /// before commit, allowing additional subtrees to be written into the same
469    /// entry that establishes the database root. See
470    /// [`Database::create_with_init`] for details on the atomicity guarantee.
471    ///
472    /// After the genesis entry commits, this method performs the standard
473    /// installation-local tracking write (key mapping and `TrackedDatabase`)
474    /// in one atomic native record replacement, separate from genesis.
475    pub async fn create_database_with_init<F>(
476        &mut self,
477        settings: Doc,
478        key_id: &PublicKey,
479        init: F,
480    ) -> Result<Database>
481    where
482        F: AsyncFnOnce(&Transaction) -> Result<()>,
483    {
484        use crate::account::types::SyncSettings;
485
486        // Get the signing key from AccountKeyManager
487        let signing_key = self
488            .key_manager
489            .get_signing_key(key_id)
490            .ok_or_else(|| AccountError::KeyNotFound {
491                key_id: key_id.to_string(),
492            })?
493            .clone();
494
495        // Create the database with the provided key directly
496        let database =
497            Database::create_with_init(&self.instance, signing_key, settings, init).await?;
498
499        let state = self
500            .update_local_state(|state| {
501                let metadata = state.keys.get_mut(&key_id.to_string()).ok_or_else(|| {
502                    AccountError::KeyNotFound {
503                        key_id: key_id.to_string(),
504                    }
505                })?;
506                metadata
507                    .database_sigkeys
508                    .insert(database.root_id().clone(), None);
509                state.databases.insert(
510                    database.root_id().to_string(),
511                    TrackedDatabase {
512                        database_id: database.root_id().clone(),
513                        key_id: key_id.clone(),
514                        sync_settings: SyncSettings::disabled(),
515                    },
516                );
517                Ok(())
518            })
519            .await?;
520        self.key_manager
521            .add_key(state.keys[&key_id.to_string()].clone())?;
522        self.refresh_local_sync().await?;
523        Ok(self.with_local_account(database))
524    }
525
526    /// Open an existing database by its root ID using this account's keys.
527    ///
528    /// This method automatically:
529    /// 1. Finds an appropriate key that has access to the database
530    /// 2. Retrieves the decrypted SigningKey from the AccountKeyManager
531    /// 3. Gets the SigKey mapping for this database
532    /// 4. Creates a Database instance configured with the account's key
533    ///
534    /// The returned Database will use the account's provided key for all operations,
535    /// without requiring backend key lookups.
536    ///
537    /// # Arguments
538    /// * `root_id` - The root entry ID of the database
539    ///
540    /// # Returns
541    /// The opened Database configured to use this account's keys
542    ///
543    /// # Errors
544    /// - Returns an error if no key is found for the database
545    /// - Returns an error if no SigKey mapping exists
546    /// - Returns an error if the key is not in the AccountKeyManager
547    pub async fn open_database(&self, root_id: &ID) -> Result<Database> {
548        // Find an appropriate key for this database
549        let key_id = self.find_key(root_id)?.ok_or_else(|| {
550            super::errors::AccountError::NoKeyForDatabase {
551                database_id: root_id.clone(),
552            }
553        })?;
554
555        self.open_database_with_key(root_id, &key_id).await
556    }
557
558    /// Open an existing database with an explicitly chosen key.
559    ///
560    /// Equivalent to `open_database`, but selects the account's signing key by
561    /// public key instead of relying on `find_key`'s iteration order. Use this
562    /// when the account holds multiple authorized keys for a database and you
563    /// need writes signed by a specific one (e.g. agent-as-signer scenarios
564    /// where cryptographic provenance matters, not just authorization).
565    ///
566    /// The `key_id` is purely a selector into this account's `AccountKeyManager`;
567    /// no crypto material is passed in.
568    ///
569    /// # Arguments
570    /// * `root_id` - The root entry ID of the database
571    /// * `key_id` - Public key of the account-held key to sign with
572    ///
573    /// # Returns
574    /// The opened Database configured to use the specified key
575    ///
576    /// # Errors
577    /// - Returns an error if the root entry does not exist
578    /// - Returns an error if the account does not hold a key with that pubkey
579    /// - Returns an error if the key has no SigKey mapping for this database
580    pub async fn open_database_with_key(
581        &self,
582        root_id: &ID,
583        key_id: &PublicKey,
584    ) -> Result<Database> {
585        // Get the SigningKey from AccountKeyManager
586        let signing_key = self.key_manager.get_signing_key(key_id).ok_or_else(|| {
587            super::errors::AccountError::KeyNotFound {
588                key_id: key_id.to_string(),
589            }
590        })?;
591
592        // Get the SigKey mapping for this database
593        let sigkey = self.key_mapping(key_id, root_id)?.ok_or_else(|| {
594            super::errors::AccountError::NoSigKeyMapping {
595                key_id: key_id.to_string(),
596                database_id: root_id.clone(),
597            }
598        })?;
599
600        // Create Database with account-provided key using resolved SigKey identity.
601        //
602        // On a connected (remote) instance, the read path must travel as the
603        // account's per-DB identity so the daemon's per-tree gate sees a key the
604        // tree actually authorises. `Database::open` would clone the
605        // instance's session backend, which on a remote instance carries the
606        // connection's login pubkey — and the account's login key is not a member
607        // of every tree they hold a per-DB key for. Route through
608        // `Database::open_remote` with the per-DB
609        // identity instead, after proving possession of the per-DB key to
610        // the daemon so the identity sits in the connection's session
611        // keyset.
612        let key = DatabaseKey::with_identity(signing_key.clone(), sigkey.clone());
613        // A SigKey mapping exists (resolved above), so the database is one this
614        // account has requested access to. If its root entry isn't present, the
615        // access is still pending — the bootstrap request hasn't been approved or
616        // the database hasn't synced yet. Surface that explicitly instead of a
617        // bare backend "not found", on both the remote and local open paths.
618        #[cfg(all(unix, feature = "service"))]
619        if let Some(conn) = self.instance.remote_connection() {
620            conn.register_session_key(signing_key).await?;
621            return match Database::open_remote(
622                &self.instance,
623                conn.with_signer(signing_key.clone()),
624                root_id,
625                sigkey,
626            )
627            .await
628            {
629                Ok(database) => Ok(self.with_local_account(database.with_key(key))),
630                Err(e) if e.is_not_found() => {
631                    Err(super::errors::AccountError::DatabaseAccessPending {
632                        database_id: root_id.clone(),
633                    }
634                    .into())
635                }
636                Err(e) => Err(e),
637            };
638        }
639        match Database::open(&self.instance, root_id).await {
640            Ok(database) => Ok(self.with_local_account(database.with_key(key))),
641            Err(e) if e.is_not_found() => Err(super::errors::AccountError::DatabaseAccessPending {
642                database_id: root_id.clone(),
643            }
644            .into()),
645            Err(e) => Err(e),
646        }
647    }
648
649    /// Find databases by name among the account's tracked databases.
650    ///
651    /// Searches only the databases this account has tracked for those matching the given name.
652    ///
653    /// # Arguments
654    /// * `name` - Database name to search for
655    ///
656    /// # Returns
657    /// Vector of matching databases from the account's tracked list
658    pub async fn find_database(&self, name: impl AsRef<str>) -> Result<Vec<Database>> {
659        let name = name.as_ref();
660        let tracked = self.databases().await?;
661        let mut matching = Vec::new();
662
663        for tracked_db in tracked {
664            if let Ok(database) = self.open_database(&tracked_db.database_id).await
665                && let Ok(db_name) = database.get_name().await
666                && db_name == name
667            {
668                matching.push(database);
669            }
670        }
671
672        if matching.is_empty() {
673            Err(AccountError::DatabaseNotFoundByName {
674                name: name.to_string(),
675            }
676            .into())
677        } else {
678            Ok(matching)
679        }
680    }
681
682    /// Find which key can access a database.
683    ///
684    /// Searches this account's keys to find one that can access the specified database.
685    /// Considers the SigKey mappings stored in account key metadata.
686    ///
687    /// Returns the key_id of a suitable key, preferring keys with mappings for this database.
688    ///
689    /// # Arguments
690    /// * `database_id` - The ID of the database
691    ///
692    /// # Returns
693    /// Some(key_id) if a suitable key is found, None if no keys can access this database
694    pub fn find_key(&self, database_id: &ID) -> Result<Option<PublicKey>> {
695        // Iterate through all keys and find ones with SigKey mappings for this database
696        for key_id in self.key_manager.list_key_ids() {
697            if let Some(metadata) = self.key_manager.get_key_metadata(&key_id)
698                && metadata.database_sigkeys.contains_key(database_id)
699            {
700                return Ok(Some(key_id));
701            }
702        }
703
704        // No key found with mapping for this database
705        Ok(None)
706    }
707
708    /// Get the resolved SigKey mapping for a key in a specific database.
709    ///
710    /// Users map their private keys to SigKey identifiers on a per-database basis.
711    /// This retrieves the resolved SigKey that a specific key uses in
712    /// a specific database's authentication settings.
713    ///
714    /// Internally, `None` in the stored mapping means "default pubkey identity",
715    /// which this method resolves to the concrete `SigKey::from_pubkey(...)` value.
716    ///
717    /// # Arguments
718    /// * `key_id` - The account's key identifier
719    /// * `database_id` - The database ID
720    ///
721    /// # Returns
722    /// `Ok(Some(sigkey))` if a mapping exists (resolved to concrete SigKey),
723    /// `Ok(None)` if no mapping is configured for this database
724    ///
725    /// # Errors
726    /// Returns an error if the key_id doesn't exist in the AccountKeyManager
727    pub fn key_mapping(&self, key_id: &PublicKey, database_id: &ID) -> Result<Option<SigKey>> {
728        let metadata = self.key_manager.get_key_metadata(key_id).ok_or_else(|| {
729            super::errors::AccountError::KeyNotFound {
730                key_id: key_id.to_string(),
731            }
732        })?;
733
734        match metadata.database_sigkeys.get(database_id) {
735            None => Ok(None), // no mapping exists
736            Some(None) => {
737                // Default: pubkey identity derived directly from key_id
738                Ok(Some(SigKey::from_pubkey(key_id)))
739            }
740            Some(Some(sigkey)) => Ok(Some(sigkey.clone())),
741        }
742    }
743
744    /// Map a key to a SigKey identity for a specific database.
745    ///
746    /// Registers that this account's key should be used with a specific SigKey identity
747    /// when interacting with a database. This is typically used when an account has been
748    /// granted access to a database and needs to configure their local key to work with it.
749    ///
750    /// If the provided SigKey matches the default pubkey identity for this key,
751    /// it is normalized to `None` internally (compact storage for the common case).
752    ///
753    /// # Multi-Key Support
754    ///
755    /// **Note**: A database may have mappings to multiple keys. This is useful for
756    /// multi-device scenarios where the same account wants to access a database from
757    /// different devices, each with their own key.
758    ///
759    /// # Arguments
760    /// * `key_id` - The account's key identifier (public key)
761    /// * `database_id` - The database ID
762    /// * `sigkey` - The SigKey identity to use for this database
763    ///
764    /// # Errors
765    /// Returns an error if the key_id doesn't exist in the account database
766    pub async fn map_key(
767        &mut self,
768        key_id: &PublicKey,
769        database_id: &ID,
770        sigkey: SigKey,
771    ) -> Result<()> {
772        let result = self
773            .update_local_state(|state| {
774                Self::map_key_in_state(state, key_id, database_id, sigkey.clone())
775            })
776            .await;
777        self.refresh_key_metadata(key_id).await?;
778        result?;
779        Ok(())
780    }
781
782    fn map_key_in_state(
783        state: &mut super::local_state::LocalAccountState,
784        key_id: &PublicKey,
785        database_id: &ID,
786        sigkey: SigKey,
787    ) -> Result<()> {
788        let metadata =
789            state
790                .keys
791                .get_mut(&key_id.to_string())
792                .ok_or_else(|| AccountError::KeyNotFound {
793                    key_id: key_id.to_string(),
794                })?;
795        let stored = if sigkey == SigKey::from_pubkey(key_id) {
796            None
797        } else {
798            Some(sigkey)
799        };
800        metadata
801            .database_sigkeys
802            .insert(database_id.clone(), stored);
803        Ok(())
804    }
805
806    async fn refresh_key_metadata(&mut self, key_id: &PublicKey) -> Result<()> {
807        let state = self.read_local_state().await?;
808        let metadata =
809            state
810                .keys
811                .get(&key_id.to_string())
812                .ok_or_else(|| AccountError::KeyNotFound {
813                    key_id: key_id.to_string(),
814                })?;
815        self.key_manager.add_key(metadata.clone())
816    }
817
818    // === Application References ===
819    //
820    // Applications keep direct database IDs in their Account under names they
821    // choose. `namespace` separates applications sharing one Account; `name` is
822    // an application alias, independent of the target's display name. Each
823    // namespace lives in its own replicated store. Keys and local tracking
824    // are outside the DAG. A reference grants no access and activates nothing.
825
826    fn reference_store(namespace: &str) -> String {
827        format!("refs:{namespace}")
828    }
829
830    fn validate_reference_name(store: &str, name: &str) -> Result<()> {
831        if name.is_empty() || name.contains('.') {
832            return Err(StoreError::InvalidOperation {
833                store: store.to_string(),
834                operation: "reference".to_string(),
835                reason: "reference names must be nonempty and contain no dots".to_string(),
836            }
837            .into());
838        }
839        Ok(())
840    }
841
842    /// Store a direct reference to a database in this Account.
843    ///
844    /// Names must be nonempty and contain no dots; they are aliases, not paths.
845    /// Overwrites any existing reference with the same namespace and name.
846    /// This is a plain CRDT write, not a compare-and-set: two initializers that
847    /// both saw no reference can both create a target and both write. Serialize
848    /// first-time initialization through one writer. To repair a partial
849    /// initialization, write the ID of the target that was already created.
850    pub async fn set_reference(&self, namespace: &str, name: &str, database_id: &ID) -> Result<()> {
851        let store = Self::reference_store(namespace);
852        Self::validate_reference_name(&store, name)?;
853        let tx = self.account.database().new_transaction().await?;
854        tx.get_store::<Table<ID>>(store)
855            .await?
856            .set(name, database_id.clone())
857            .await?;
858        tx.commit().await?;
859        Ok(())
860    }
861
862    /// Read a reference stored with [`Self::set_reference`].
863    ///
864    /// Names must be nonempty and contain no dots.
865    /// Returns `Ok(None)` only when the Account has no such reference. Every
866    /// other failure, including unreadable or malformed reference data, is an
867    /// error and must not be treated as permission to create a replacement.
868    /// Reads only this reference; no other database is opened.
869    pub async fn reference(&self, namespace: &str, name: &str) -> Result<Option<ID>> {
870        let store = Self::reference_store(namespace);
871        Self::validate_reference_name(&store, name)?;
872        // Table's projection omits non-text values; read raw data so corruption
873        // cannot masquerade as an absent reference or a stale projected ID.
874        let references = self
875            .account
876            .database()
877            .get_store_viewer::<DocStore>(store.as_str())
878            .await?;
879        match references.get_string(name).await {
880            Ok(value) => serde_json::from_str(&value).map(Some).map_err(|e| {
881                StoreError::DeserializationFailed {
882                    store: store.clone(),
883                    reason: format!("Invalid database reference '{name}': {e}"),
884                }
885                .into()
886            }),
887            Err(Error::Store(e))
888                if matches!(e.as_ref(), StoreError::KeyNotFound { store: s, key }
889                    if *s == store && key == name) =>
890            {
891                Ok(None)
892            }
893            Err(e) => Err(e),
894        }
895    }
896
897    /// Open the database named by a reference.
898    ///
899    /// Returns `Ok(None)` only when the reference is absent. A present
900    /// reference opens through [`Self::open_database`], so a missing key,
901    /// pending access or unavailable target is an error, never `None`.
902    pub async fn open_reference(&self, namespace: &str, name: &str) -> Result<Option<Database>> {
903        match self.reference(namespace, name).await? {
904            Some(id) => self.open_database(&id).await.map(Some),
905            None => Ok(None),
906        }
907    }
908
909    // === Key Management (AccountSession Context) ===
910
911    /// Add a new private key to this account's keyring.
912    ///
913    /// Generates a new Ed25519 keypair. Registered sessions encrypt it in the
914    /// native complete bundle and atomically update its metadata, including for
915    /// empty-password accounts. Caller-held
916    /// sessions update only their in-memory bundle; persistence is explicit.
917    ///
918    /// # Arguments
919    /// * `display_name` - Optional display name for the key
920    ///
921    /// # Returns
922    /// The key ID (public key string)
923    pub async fn add_private_key(&mut self, display_name: Option<&str>) -> Result<PublicKey> {
924        self.import_private_key(crate::auth::crypto::generate_keypair().0, display_name)
925            .await
926    }
927
928    /// Add exactly one caller-supplied key to this local bundle. Duplicate keys
929    /// fail without replacing metadata. This neither enrolls an Identity member
930    /// nor creates target mappings; database Auth remains independent of custody.
931    /// Caller-held sessions still require explicit external persistence.
932    pub async fn import_private_key(
933        &mut self,
934        private_key: crate::auth::crypto::PrivateKey,
935        display_name: Option<&str>,
936    ) -> Result<PublicKey> {
937        let public_key = private_key.public_key();
938        let metadata = super::AccountKey {
939            key_id: public_key.clone(),
940            display_name: display_name.map(str::to_owned),
941            created_at: self.instance.clock().now_secs(),
942            last_used: None,
943            is_default: false,
944            database_sigkeys: HashMap::new(),
945        };
946        // Caller-held keys remain in memory. No wrapping password or durable
947        // credential record is required for explicit signer sessions.
948        if let Some(state) = &self.caller_state {
949            let mut state = state.lock().await;
950            self.key_manager.import_key(private_key, metadata.clone())?;
951            state.keys.insert(public_key.to_string(), metadata);
952            return Ok(public_key);
953        }
954        let state = self
955            .update_local_state(|state| {
956                let storage = state
957                    .credentials
958                    .as_ref()
959                    .ok_or(AccountError::InvalidLocalState)?;
960                // Merge against the observed complete bundle, not a stale key list:
961                // concurrent key additions must not overwrite one another.
962                let mut credentials = self.key_manager.unlock_storage(storage)?;
963                credentials.add_key(private_key.clone())?;
964                state.credentials = Some(credentials.storage()?);
965                state.keys.insert(public_key.to_string(), metadata.clone());
966                Ok(())
967            })
968            .await?;
969        let credentials = self.key_manager.unlock_storage(
970            state
971                .credentials
972                .as_ref()
973                .ok_or(AccountError::InvalidLocalState)?,
974        )?;
975        self.key_manager = AccountKeyManager::new(credentials, state.keys.into_values().collect())?;
976
977        Ok(public_key)
978    }
979
980    /// List all key IDs owned by this account.
981    ///
982    /// Keys are sorted by creation timestamp (oldest first), then public key ID
983    /// for ties. Use [`Self::get_default_key`] for the explicit default.
984    ///
985    /// # Returns
986    /// Vector of PublicKeys sorted by creation time
987    pub fn list_keys(&self) -> Result<Vec<PublicKey>> {
988        Ok(self.key_manager.list_key_ids())
989    }
990
991    /// Get the default key.
992    ///
993    /// Returns the key marked as is_default=true, or falls back to the oldest key
994    /// by creation timestamp if no default is explicitly set.
995    ///
996    /// # Returns
997    /// The PublicKey of the default key
998    ///
999    /// # Errors
1000    /// Returns an error if no keys exist
1001    pub fn get_default_key(&self) -> Result<PublicKey> {
1002        self.key_manager
1003            .get_default_key_id()
1004            .ok_or_else(|| Error::from(InstanceError::AuthenticationRequired))
1005    }
1006
1007    /// Get the display name set for a key.
1008    ///
1009    /// Display names are caller-supplied human-readable labels passed to
1010    /// `add_private_key`. They have no cryptographic significance and are
1011    /// not unique across keys. Returns `None` if the account does not hold a
1012    /// key with this pubkey, or if it was added without a display name.
1013    ///
1014    /// # Arguments
1015    /// * `key_id` - Public key of a key held by this account
1016    pub fn key_display_name(&self, key_id: &PublicKey) -> Option<&str> {
1017        self.key_manager
1018            .get_key_metadata(key_id)
1019            .and_then(|metadata| metadata.display_name.as_deref())
1020    }
1021
1022    /// Find account-held keys whose display name matches `name`.
1023    ///
1024    /// Display names are not unique, so this returns every key whose
1025    /// `display_name` exactly matches the provided string. Keys without a
1026    /// display name are never returned. Returns an empty `Vec` when there
1027    /// are no matches.
1028    ///
1029    /// # Arguments
1030    /// * `name` - Exact display name to match
1031    pub fn find_keys_by_display_name(&self, name: &str) -> Vec<PublicKey> {
1032        self.key_manager
1033            .list_key_ids()
1034            .into_iter()
1035            .filter(|key_id| {
1036                self.key_manager
1037                    .get_key_metadata(key_id)
1038                    .and_then(|metadata| metadata.display_name.as_deref())
1039                    == Some(name)
1040            })
1041            .collect()
1042    }
1043
1044    /// Get a signing key by its ID.
1045    ///
1046    /// Hands out key material from this session's unlocked key manager. Callers
1047    /// that sign on the account's behalf need it — signing a sync request as the
1048    /// key whose access they are claiming, for example.
1049    ///
1050    /// # Arguments
1051    /// * `key_id` - The public key identifier
1052    ///
1053    /// # Returns
1054    /// The PrivateKey if found
1055    pub fn get_signing_key(&self, key_id: &PublicKey) -> Result<crate::auth::crypto::PrivateKey> {
1056        self.key_manager
1057            .get_signing_key(key_id)
1058            .cloned()
1059            .ok_or_else(|| {
1060                AccountError::KeyNotFound {
1061                    key_id: key_id.to_string(),
1062                }
1063                .into()
1064            })
1065    }
1066
1067    // === Bootstrap Request Management (AccountSession Context) ===
1068
1069    /// Get all pending bootstrap requests from the sync system.
1070    ///
1071    /// This is a convenience method that requires the Instance's Sync to be initialized.
1072    ///
1073    /// # Arguments
1074    /// * `sync` - Reference to the Instance's Sync object
1075    ///
1076    /// # Returns
1077    /// A vector of (request_id, bootstrap_request) pairs for pending requests
1078    pub async fn pending_bootstrap_requests(
1079        &self,
1080        sync: &Sync,
1081    ) -> Result<Vec<(String, BootstrapRequest)>> {
1082        sync.pending_bootstrap_requests().await
1083    }
1084
1085    /// Approve a bootstrap request and add the requesting key to the target database.
1086    ///
1087    /// The approving key must have current Admin permission on the target database.
1088    /// Creating a grant also requires authority at least as high as the requested
1089    /// permission and priority; lower priority numbers have higher authority.
1090    ///
1091    /// # Arguments
1092    /// * `sync` - Mutable reference to the Instance's Sync object
1093    /// * `request_id` - The unique identifier of the request to approve
1094    /// * `approving_key_id` - The ID of this account's key to use for approval (must have Admin permission)
1095    ///
1096    /// # Returns
1097    /// Result indicating success or failure of the approval operation
1098    ///
1099    /// # Errors
1100    /// - Returns an error if the account doesn't own the specified approving key
1101    /// - Returns an error if the approving key doesn't have Admin permission on the target database
1102    /// - Returns an error if the request doesn't exist or was rejected
1103    /// - Reduced, removed or uncertain authority needs a fresh explicit grant
1104    /// - Returns an error if the key addition to the database fails
1105    pub async fn approve_bootstrap_request(
1106        &self,
1107        sync: &Sync,
1108        request_id: &str,
1109        approving_key_id: &PublicKey,
1110    ) -> Result<()> {
1111        // Get the signing key from the key manager
1112        let signing_key = self
1113            .key_manager
1114            .get_signing_key(approving_key_id)
1115            .ok_or_else(|| super::errors::AccountError::KeyNotFound {
1116                key_id: approving_key_id.to_string(),
1117            })?;
1118
1119        // Delegate to Sync layer with the account-provided key
1120        // The Sync layer will validate permissions when committing the transaction
1121        let key = DatabaseKey::new(signing_key.clone());
1122        sync.approve_bootstrap_request_with_key(request_id, &key)
1123            .await?;
1124
1125        Ok(())
1126    }
1127
1128    /// Reject a bootstrap request.
1129    ///
1130    /// This method marks the request as rejected. The requesting device will not
1131    /// be granted access to the target database. Requires Admin permission on the
1132    /// target database to prevent unauthorized accounts from disrupting the bootstrap protocol.
1133    ///
1134    /// # Arguments
1135    /// * `sync` - Mutable reference to the Instance's Sync object
1136    /// * `request_id` - The unique identifier of the request to reject
1137    /// * `rejecting_key_id` - The ID of this account's key (for permission validation and audit trail)
1138    ///
1139    /// # Returns
1140    /// Result indicating success or failure of the rejection operation
1141    ///
1142    /// # Errors
1143    /// - Returns an error if the account doesn't own the specified rejecting key
1144    /// - Returns an error if the request doesn't exist or was approved
1145    /// - Returns an error if the rejecting key lacks Admin permission on the target database
1146    pub async fn reject_bootstrap_request(
1147        &self,
1148        sync: &Sync,
1149        request_id: &str,
1150        rejecting_key_id: &PublicKey,
1151    ) -> Result<()> {
1152        // Get the signing key from the key manager
1153        let signing_key = self
1154            .key_manager
1155            .get_signing_key(rejecting_key_id)
1156            .ok_or_else(|| super::errors::AccountError::KeyNotFound {
1157                key_id: rejecting_key_id.to_string(),
1158            })?;
1159
1160        // Delegate to Sync layer with the account-provided key
1161        // The Sync layer will validate Admin permission on the target database
1162        let key = DatabaseKey::new(signing_key.clone());
1163        sync.reject_bootstrap_request_with_key(request_id, &key)
1164            .await?;
1165
1166        Ok(())
1167    }
1168
1169    /// Request access to a database from a peer (bootstrap sync).
1170    ///
1171    /// This convenience method initiates a bootstrap sync request to access a database
1172    /// that this account doesn't have locally yet. The account's key will be sent to the peer
1173    /// to request the specified permission level.
1174    ///
1175    /// This is useful for multi-device scenarios where an account wants to access their
1176    /// existing database from a new device, or when requesting access to a database
1177    /// shared by another account.
1178    ///
1179    /// On a successful (already-authorized) request the database is synced and its
1180    /// SigKey mapping is recorded, so [`open_database`](Self::open_database) works
1181    /// immediately — no separate [`track_database`](Self::track_database) call is
1182    /// needed just to open it. The supplied `sync_settings` are recorded with
1183    /// the database, including when a pending request is retried after approval.
1184    ///
1185    /// When approval is still pending the request returns
1186    /// [`SyncError::BootstrapPending`] without changing Account mappings or
1187    /// preferences. Supply the arguments again when retrying after approval.
1188    /// Existing legacy mappings are preserved rather than deleted.
1189    ///
1190    /// # Arguments
1191    /// * `sync` - Reference to the Instance's Sync object
1192    /// * `ticket` - A ticket containing the database ID and address hints
1193    /// * `key_id` - The ID of this account's key to use for the request
1194    /// * `requested_permission` - The permission level being requested
1195    /// * `sync_settings` - This account's durable synchronization preferences for
1196    ///   the database once its access mapping is recorded
1197    /// * `metadata` - Optional free-form context for the approver to inspect on
1198    ///   the stored bootstrap request when deciding whether to grant access
1199    ///
1200    /// # Returns
1201    /// Result indicating success or failure of the bootstrap request
1202    ///
1203    /// # Errors
1204    /// - Returns an error if the account doesn't own the specified key
1205    /// - Returns an error if all addresses in the ticket fail
1206    /// - Returns an error if the bootstrap sync fails
1207    ///
1208    /// # Example
1209    /// ```rust,ignore
1210    /// // Request write access to a shared database
1211    /// let user_key_id = account.get_default_key()?;
1212    /// let ticket: DatabaseTicket = "eidetica:?db=sha256:abc...&pr=http:192.168.1.1:8080".parse()?;
1213    /// account.request_database_access(
1214    ///     &sync,
1215    ///     &ticket,
1216    ///     &user_key_id,
1217    ///     Permission::Write(5),
1218    ///     SyncSettings::on_commit(),
1219    ///     None,
1220    /// ).await?;
1221    ///
1222    /// // After approval, the database can be opened
1223    /// let database = account.open_database(ticket.database_id())?;
1224    /// ```
1225    pub async fn request_database_access(
1226        &mut self,
1227        sync: &Sync,
1228        ticket: &DatabaseTicket,
1229        key_id: &PublicKey,
1230        requested_permission: Permission,
1231        sync_settings: SyncSettings,
1232        metadata: Option<Doc>,
1233    ) -> Result<()> {
1234        // The request is signed with this key, so the peer can tell an actual
1235        // key holder from someone naming a key they don't have.
1236        let signing_key = self
1237            .key_manager
1238            .get_signing_key(key_id)
1239            .ok_or_else(|| super::errors::AccountError::KeyNotFound {
1240                key_id: key_id.to_string(),
1241            })?
1242            .clone();
1243
1244        let key_name = key_id.to_string();
1245        let database_id = ticket.database_id().clone();
1246        let original = self.tracked_database(&database_id).await?;
1247
1248        let result = sync
1249            .bootstrap_with_ticket(
1250                ticket,
1251                &signing_key,
1252                &key_name,
1253                requested_permission,
1254                metadata,
1255            )
1256            .await;
1257
1258        self.record_database_access_with_expected(
1259            &database_id,
1260            key_id,
1261            sync_settings,
1262            result,
1263            Some(original),
1264        )
1265        .await
1266    }
1267
1268    /// Join a database from a ticket and open it.
1269    ///
1270    /// Works the same on an embedded and a connected instance. An embedded
1271    /// instance bootstraps through its own [`Sync`], which must be enabled.
1272    /// A connected instance asks the daemon to do it: the daemon chooses the
1273    /// route and owns the network exchange, while `key_id`'s private key
1274    /// stays in this process and signs only the peer-bound request proof. The
1275    /// call waits until the peer answers.
1276    ///
1277    /// A ticket locates a database; it does not grant access. The peer decides
1278    /// whether `key_id` may have `requested_permission`, exactly as for
1279    /// [`request_database_access`](Self::request_database_access), whose
1280    /// mapping and preference semantics this shares:
1281    ///
1282    /// - When access is granted, the database is synced, `key_id`'s SigKey
1283    ///   mapping and `sync_settings` are recorded, and the opened database is
1284    ///   returned.
1285    /// - When approval is pending, this returns [`SyncError::BootstrapPending`]
1286    ///   without changing Account mappings or preferences. Call `join` again with
1287    ///   the same arguments once the request is approved.
1288    /// - A rejected request returns [`SyncError::BootstrapRejected`] and
1289    ///   records nothing.
1290    ///
1291    /// Success is point-in-time usability of the verified supplied closure,
1292    /// not global catch-up. Identical completed settings are not written again;
1293    /// preferences changed during the network wait are preserved.
1294    /// Dropping the future, disconnecting, or an error may leave peer requests,
1295    /// replicated entries, routing or a persisted local account record without its ack.
1296    /// Retry explicitly; nothing automatically resumes a join after restart.
1297    /// Held/unreadable acquisition remains refused. Missing delegated history
1298    /// stays Unverified until supplied and verified separately; this call does
1299    /// not acquire it automatically. Native conflicting authority is unchanged,
1300    /// so recovery from every interrupted transfer is not guaranteed.
1301    /// Joining grants user-key access, not device enrollment. Later daemon
1302    /// replication uses its device key and needs separate authorization (or a
1303    /// global grant); this call does not promise ongoing private replication.
1304    ///
1305    /// # Arguments
1306    /// * `ticket` - Database ID and address hints
1307    /// * `key_id` - This user's key to request access for
1308    /// * `requested_permission` - The permission level being requested
1309    /// * `sync_settings` - This user's replication preference for the database
1310    /// * `metadata` - Optional context for the approver
1311    ///
1312    /// # Errors
1313    /// - [`AccountError::KeyNotFound`] if this user does not hold `key_id`
1314    /// - [`SyncError::SyncNotEnabled`] on an embedded instance without sync
1315    /// - [`SyncError::BootstrapPending`] or [`SyncError::BootstrapRejected`]
1316    ///   as described above
1317    /// - Any network or sync error if no ticket address answered
1318    ///
1319    /// # Example
1320    /// ```rust,ignore
1321    /// let key_id = user.get_default_key()?;
1322    /// let database = user
1323    ///     .join(&ticket, &key_id, Permission::Write(10), SyncSettings::on_commit(), None)
1324    ///     .await?;
1325    /// ```
1326    pub async fn join(
1327        &mut self,
1328        ticket: &DatabaseTicket,
1329        key_id: &PublicKey,
1330        requested_permission: Permission,
1331        sync_settings: SyncSettings,
1332        metadata: Option<Doc>,
1333    ) -> Result<Database> {
1334        let database_id = ticket.database_id().clone();
1335
1336        #[cfg(all(unix, feature = "service"))]
1337        if let Some(connection) = self.instance.remote_connection() {
1338            let original = self.tracked_database(&database_id).await?;
1339            let signing_key = self
1340                .key_manager
1341                .get_signing_key(key_id)
1342                .ok_or_else(|| super::errors::AccountError::KeyNotFound {
1343                    key_id: key_id.to_string(),
1344                })?
1345                .clone();
1346            let result = connection
1347                .bootstrap_ticket(
1348                    ticket,
1349                    &signing_key,
1350                    &key_id.to_string(),
1351                    requested_permission,
1352                    metadata,
1353                    self.instance.clock().now_millis(),
1354                )
1355                .await;
1356            self.record_database_access_with_expected(
1357                &database_id,
1358                key_id,
1359                sync_settings,
1360                result,
1361                Some(original),
1362            )
1363            .await?;
1364            return self
1365                .open_joined_database(&database_id, key_id, requested_permission)
1366                .await;
1367        }
1368
1369        let sync = self.instance.sync().ok_or(SyncError::SyncNotEnabled)?;
1370        self.request_database_access(
1371            &sync,
1372            ticket,
1373            key_id,
1374            requested_permission,
1375            sync_settings,
1376            metadata,
1377        )
1378        .await?;
1379        self.open_joined_database(&database_id, key_id, requested_permission)
1380            .await
1381    }
1382
1383    async fn open_joined_database(
1384        &self,
1385        database_id: &ID,
1386        key_id: &PublicKey,
1387        requested_permission: Permission,
1388    ) -> Result<Database> {
1389        let database = self.open_database_with_key(database_id, key_id).await?;
1390        if database.current_permission().await? < requested_permission {
1391            return Err(SyncError::PermissionDenied(
1392                "requested permission is no longer available at join completion".to_string(),
1393            )
1394            .into());
1395        }
1396        Ok(database)
1397    }
1398
1399    /// Record the Account-layer SigKey mapping for a bootstrap whose network phase
1400    /// has already completed, given the [`Result`] returned by
1401    /// [`Sync::bootstrap_with_ticket`](crate::sync::Sync::bootstrap_with_ticket).
1402    ///
1403    /// [`request_database_access`](Self::request_database_access) is the usual
1404    /// entry point and performs the network bootstrap for you. This split-out
1405    /// method exists for callers that hold a coarse lock around the `AccountSession` (e.g.
1406    /// a daemon's per-session lock): they can run the network round-trip without
1407    /// the lock held and re-acquire it only for this cheap, local mapping write,
1408    /// so a slow or hung peer never blocks the rest of the session. The mapping
1409    /// semantics are identical to `request_database_access`. The split form
1410    /// treats supplied preferences as current explicit intent; use
1411    /// [`record_database_access_if_unchanged`](Self::record_database_access_if_unchanged)
1412    /// to preserve a tracked row changed during the network wait.
1413    ///
1414    /// The `bootstrap_result` is consumed and its outcome re-raised unchanged so
1415    /// callers can react to [`SyncError::BootstrapPending`] et al.
1416    pub async fn record_database_access(
1417        &mut self,
1418        database_id: &ID,
1419        key_id: &PublicKey,
1420        sync_settings: SyncSettings,
1421        bootstrap_result: Result<()>,
1422    ) -> Result<()> {
1423        self.record_database_access_with_expected(
1424            database_id,
1425            key_id,
1426            sync_settings,
1427            bootstrap_result,
1428            None,
1429        )
1430        .await
1431    }
1432
1433    /// Record a completed bootstrap without replacing tracked preferences changed
1434    /// since the caller observed `original` before its network wait.
1435    ///
1436    /// Like [`record_database_access`](Self::record_database_access), this reports
1437    /// Pending, Rejected and network errors without writing Account metadata.
1438    /// After successful access, SigKey mapping repair still proceeds; the tracked
1439    /// row is only updated if it still equals `original`. This is an observed-state
1440    /// check, not distributed compare-and-set. Local persistence errors remain
1441    /// errors even when the network grant or local record replacement already succeeded.
1442    pub async fn record_database_access_if_unchanged(
1443        &mut self,
1444        database_id: &ID,
1445        key_id: &PublicKey,
1446        sync_settings: SyncSettings,
1447        bootstrap_result: Result<()>,
1448        original: Option<TrackedDatabase>,
1449    ) -> Result<()> {
1450        self.record_database_access_with_expected(
1451            database_id,
1452            key_id,
1453            sync_settings,
1454            bootstrap_result,
1455            Some(original),
1456        )
1457        .await
1458    }
1459
1460    async fn record_database_access_with_expected(
1461        &mut self,
1462        database_id: &ID,
1463        key_id: &PublicKey,
1464        sync_settings: SyncSettings,
1465        bootstrap_result: Result<()>,
1466        original: Option<Option<TrackedDatabase>>,
1467    ) -> Result<()> {
1468        // Pending, Rejected and failures do not create or delete AccountSession state.
1469        bootstrap_result?;
1470        self.track_database_with_expected(database_id.clone(), key_id, sync_settings, original)
1471            .await
1472    }
1473
1474    // === Tracked Databases ===
1475
1476    /// Read this account's local tracked state for one database.
1477    ///
1478    /// Returns `Ok(None)` only when the tracking row is absent. Authorization,
1479    /// storage read and decoding failures are returned unchanged, not treated as
1480    /// an untracked database or a disabled sync preference.
1481    ///
1482    /// Reads the native login record (or caller-held session state), never a
1483    /// replicated Table. Does not open the target database, track/join it, or
1484    /// change keys or sync settings. The returned settings are this account's
1485    /// local preference, not combined sync state or proof of target access.
1486    pub async fn tracked_database(&self, database_id: &ID) -> Result<Option<TrackedDatabase>> {
1487        Ok(self
1488            .read_local_state()
1489            .await?
1490            .databases
1491            .get(&database_id.to_string())
1492            .cloned())
1493    }
1494
1495    /// Track a database, adding it to this account's list with auto-discovery of SigKeys.
1496    ///
1497    /// This method adds an existing database to your tracked list, or updates it if
1498    /// already tracked (upsert behavior).
1499    ///
1500    /// When tracking:
1501    /// 1. Uses Database::find_sigkeys() to discover which SigKey the account can use
1502    /// 2. Automatically selects the SigKey with highest permission
1503    /// 3. Stores the key mapping and sync settings
1504    ///
1505    /// The sync_settings indicate your sync preferences, but do not automatically
1506    /// configure sync. Use the Sync module's peer and tree methods to set up actual
1507    /// sync relationships.
1508    ///
1509    /// # Arguments
1510    /// * `database_id` - ID of the database to track
1511    /// * `key_id` - Which account key to use for this database
1512    /// * `sync_settings` - Sync preferences for this database
1513    ///
1514    /// # Returns
1515    /// Result indicating success or failure
1516    ///
1517    /// # Errors
1518    /// - Returns `NoSigKeyFound` if no SigKey can be found for the specified key
1519    /// - Returns `KeyNotFound` if the specified key_id doesn't exist
1520    pub async fn track_database(
1521        &mut self,
1522        database_id: impl Into<ID>,
1523        key_id: &PublicKey,
1524        sync_settings: SyncSettings,
1525    ) -> Result<()> {
1526        self.track_database_with_expected(database_id.into(), key_id, sync_settings, None)
1527            .await
1528    }
1529
1530    async fn track_database_with_expected(
1531        &mut self,
1532        database_id: ID,
1533        key_id: &PublicKey,
1534        sync_settings: SyncSettings,
1535        original: Option<Option<TrackedDatabase>>,
1536    ) -> Result<()> {
1537        let tracked = TrackedDatabase {
1538            database_id,
1539            key_id: key_id.clone(),
1540            sync_settings,
1541        };
1542        if self.key_manager.get_signing_key(key_id).is_none() {
1543            return Err(AccountError::KeyNotFound {
1544                key_id: key_id.to_string(),
1545            }
1546            .into());
1547        }
1548        // Discovery can read delegated databases, so do it before holding the
1549        // AccountSession write lock. Even an unchanged tracked key needs a valid identity.
1550        let available =
1551            Database::find_sigkeys(&self.instance, &tracked.database_id, key_id).await?;
1552        let (sigkey, _) = available
1553            .first()
1554            .ok_or_else(|| AccountError::NoSigKeyFound {
1555                key_id: key_id.to_string(),
1556                database_id: tracked.database_id.clone(),
1557            })?;
1558        let result = self
1559            .update_local_state(|state| {
1560                Self::map_key_in_state(state, key_id, &tracked.database_id, sigkey.clone())?;
1561                let key = tracked.database_id.to_string();
1562                let existing = state.databases.get(&key).cloned();
1563                // Preserve choices changed by another session while bootstrap waited.
1564                if original.as_ref().is_none_or(|before| *before == existing) {
1565                    state.databases.insert(key, tracked.clone());
1566                }
1567                Ok(())
1568            })
1569            .await;
1570        // A lost acknowledgement is not rollback: reconcile from persisted
1571        // local bytes before reporting the original persistence outcome.
1572        self.refresh_key_metadata(key_id).await?;
1573        result?;
1574        self.refresh_local_sync().await?;
1575        Ok(())
1576    }
1577
1578    /// List all tracked databases.
1579    ///
1580    /// Returns all databases this account has added to their tracked list.
1581    ///
1582    /// # Returns
1583    /// Vector of TrackedDatabase entries
1584    pub async fn databases(&self) -> Result<Vec<TrackedDatabase>> {
1585        Ok(self
1586            .read_local_state()
1587            .await?
1588            .databases
1589            .into_values()
1590            .collect())
1591    }
1592
1593    /// Get a specific tracked database by ID.
1594    ///
1595    /// # Arguments
1596    /// * `database_id` - The ID of the database
1597    ///
1598    /// # Returns
1599    /// The TrackedDatabase if it's in the account's tracked list
1600    ///
1601    /// # Errors
1602    /// Returns `DatabaseNotTracked` if the database is not in the account's list
1603    pub async fn database(&self, database_id: &ID) -> Result<TrackedDatabase> {
1604        self.tracked_database(database_id).await?.ok_or_else(|| {
1605            AccountError::DatabaseNotTracked {
1606                database_id: database_id.clone(),
1607            }
1608            .into()
1609        })
1610    }
1611
1612    /// Check whether this account has sync enabled for a tracked database.
1613    ///
1614    /// Returns this account's own preference, not the host's combined state.
1615    /// A `false` result means this account is not personally sharing the database;
1616    /// another account on the same instance may still have sync enabled for it.
1617    ///
1618    /// Returns `Ok(false)` for databases not tracked by this account.
1619    pub async fn is_sync_enabled(&self, database_id: &ID) -> Result<bool> {
1620        Ok(self
1621            .tracked_database(database_id)
1622            .await?
1623            .is_some_and(|tracked| tracked.sync_settings.sync_enabled))
1624    }
1625
1626    /// Update this account's sync preferences for an already tracked database.
1627    ///
1628    /// Requires the recorded key/mapping to still authorize reading the database.
1629    /// Does not discover or repair mappings, track a new database, or change
1630    /// additional sync properties. Native local compare/exchange preserves other
1631    /// sessions' edits and triggers ordinary sync reconciliation; this is not
1632    /// global runtime control or distributed CAS. Caller-held sessions only
1633    /// update their session-local state, without enabling background sync.
1634    /// A failed acknowledgement is not proof of rollback; reload before retrying.
1635    pub async fn set_sync_preferences(
1636        &mut self,
1637        database_id: &ID,
1638        enabled: bool,
1639        on_commit: bool,
1640        interval_seconds: Option<u64>,
1641    ) -> Result<()> {
1642        let tracked = self.database(database_id).await?;
1643        let database = self
1644            .open_database_with_key(database_id, &tracked.key_id)
1645            .await?;
1646        if database.current_permission().await? < Permission::Read {
1647            return Err(AccountError::InsufficientPermissions.into());
1648        }
1649        self.update_local_state(|state| {
1650            let current = state
1651                .databases
1652                .get_mut(&database_id.to_string())
1653                .ok_or_else(|| AccountError::DatabaseNotTracked {
1654                    database_id: database_id.clone(),
1655                })?;
1656            // Do not apply a preference edit to a mapping replaced while the
1657            // recorded key's live authority was being checked.
1658            if current.key_id != tracked.key_id {
1659                return Err(AccountError::InsufficientPermissions.into());
1660            }
1661            current.sync_settings.sync_enabled = enabled;
1662            current.sync_settings.sync_on_commit = on_commit;
1663            current.sync_settings.interval_seconds = interval_seconds;
1664            Ok(())
1665        })
1666        .await?;
1667        self.refresh_local_sync().await?;
1668        Ok(())
1669    }
1670
1671    /// Stop tracking a database.
1672    ///
1673    /// This removes the database from the account's tracked list.
1674    /// It does not delete the database itself, remove key mappings, or delete any data.
1675    ///
1676    /// # Arguments
1677    /// * `database_id` - The ID of the database to stop tracking
1678    ///
1679    /// # Errors
1680    /// Returns `DatabaseNotTracked` if the database is not in the account's list
1681    pub async fn untrack_database(&mut self, database_id: &ID) -> Result<()> {
1682        self.update_local_state(|state| {
1683            state
1684                .databases
1685                .remove(&database_id.to_string())
1686                .ok_or_else(|| AccountError::DatabaseNotTracked {
1687                    database_id: database_id.clone(),
1688                })?;
1689            Ok(())
1690        })
1691        .await?;
1692        self.refresh_local_sync().await?;
1693        Ok(())
1694    }
1695}