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}