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

eidetica/sync/
handler.rs

1//! Sync request handler trait and implementation.
2//!
3//! This module contains transport-agnostic handlers that process
4//! sync requests and generate responses. These handlers can be
5//! used by any transport implementation through the SyncHandler trait.
6
7use std::collections::BTreeMap;
8
9use async_trait::async_trait;
10use tracing::{Instrument, debug, error, info, info_span, trace, warn};
11
12use super::{
13    bootstrap_request_manager::{BootstrapRequest, BootstrapRequestManager, RequestStatus},
14    peer_manager::PeerManager,
15    peer_types::Address,
16    protocol::{
17        BootstrapResponse, HandshakeRequest, HandshakeResponse, IncrementalResponse,
18        PROTOCOL_VERSION, RequestContext, SyncRequest, SyncResponse, SyncTreeRequest,
19    },
20    user_sync_manager::UserSyncManager,
21};
22use crate::{
23    Database, Entry, Error, Instance, Result, WeakInstance,
24    auth::{
25        KeyStatus, Permission,
26        crypto::{PublicKey, create_challenge_response, generate_challenge},
27    },
28    entry::ID,
29    store::SettingsStore,
30    sync::error::SyncError,
31};
32
33/// Trait for handling sync requests with database access.
34///
35/// Implementations of this trait can process sync requests and generate
36/// appropriate responses, with full access to the database backend for
37/// storing and retrieving entries.
38#[async_trait]
39pub trait SyncHandler: Send + std::marker::Sync {
40    /// Handle a sync request and generate an appropriate response.
41    ///
42    /// This is the main entry point for processing sync messages,
43    /// regardless of which transport they arrived through.
44    ///
45    /// # Arguments
46    /// * `request` - The sync request to process
47    /// * `context` - Context about the request (remote address, etc.)
48    ///
49    /// # Returns
50    /// The appropriate response for the given request.
51    async fn handle_request(&self, request: &SyncRequest, context: &RequestContext)
52    -> SyncResponse;
53}
54
55/// Default implementation of SyncHandler with database backend access.
56pub struct SyncHandlerImpl {
57    instance: WeakInstance,
58    sync_tree_id: ID,
59}
60
61impl SyncHandlerImpl {
62    /// Create a new SyncHandlerImpl with the given instance.
63    ///
64    /// # Arguments
65    /// * `instance` - Database instance for storing and retrieving entries
66    /// * `sync_tree_id` - Root ID of the sync database for storing bootstrap requests
67    pub fn new(instance: Instance, sync_tree_id: ID) -> Self {
68        Self {
69            instance: instance.downgrade(),
70            sync_tree_id,
71        }
72    }
73
74    /// Upgrade the weak instance reference to a strong reference.
75    pub(super) fn instance(&self) -> Result<Instance> {
76        self.instance
77            .upgrade()
78            .ok_or_else(|| SyncError::InstanceDropped.into())
79    }
80
81    /// Get access to the sync tree for bootstrap request management.
82    ///
83    /// # Returns
84    /// A Database instance for the sync tree with device key authentication.
85    async fn get_sync_tree(&self) -> Result<Database> {
86        // Load sync tree with the device key
87        let instance = self.instance()?;
88        let signing_key = instance.signing_key()?.clone();
89        Ok(Database::open(&instance, &self.sync_tree_id)
90            .await?
91            .with_key(signing_key))
92    }
93
94    /// Store a bootstrap request in the sync database for manual approval.
95    ///
96    /// # Arguments
97    /// * `tree_id` - ID of the tree being requested
98    /// * `requesting_key` - Public key of the requesting device
99    /// * `requesting_key_name` - Name of the requesting key
100    /// * `requested_permission` - Permission level being requested
101    ///
102    /// # Returns
103    /// The generated UUID for the stored request
104    async fn store_bootstrap_request(
105        &self,
106        tree_id: &ID,
107        requesting_key: &PublicKey,
108        requesting_key_name: &str,
109        requested_permission: &Permission,
110    ) -> Result<String> {
111        let sync_tree = self.get_sync_tree().await?;
112        let txn = sync_tree.new_transaction().await?;
113        let manager = BootstrapRequestManager::new(&txn);
114
115        let request = BootstrapRequest {
116            tree_id: tree_id.clone(),
117            requesting_pubkey: requesting_key.clone(),
118            requesting_key_name: requesting_key_name.to_string(),
119            requested_permission: *requested_permission,
120            timestamp: self.instance()?.clock().now_rfc3339(),
121            status: RequestStatus::Pending,
122            // TODO: We need to get the actual peer address from the transport layer
123            // For now, use a placeholder that will need to be fixed when implementing notifications
124            peer_address: Address {
125                transport_type: "unknown".to_string(),
126                address: "unknown".to_string(),
127            },
128        };
129
130        let request_id = manager.store_request(request).await?;
131        txn.commit().await?;
132
133        Ok(request_id)
134    }
135}
136
137#[async_trait]
138impl SyncHandler for SyncHandlerImpl {
139    async fn handle_request(
140        &self,
141        request: &SyncRequest,
142        context: &RequestContext,
143    ) -> SyncResponse {
144        match request {
145            SyncRequest::Handshake(handshake_req) => {
146                debug!("Received handshake request");
147                self.handle_handshake(handshake_req, context).await
148            }
149            SyncRequest::SyncTree(sync_req) => {
150                debug!(tree_id = %sync_req.tree_id, tips_count = sync_req.our_tips.len(), "Received sync tree request");
151                self.handle_sync_tree(sync_req, context).await
152            }
153            SyncRequest::SendEntries(entries) => {
154                // Process and store the received entries
155                let count = entries.len();
156                info!(count = count, "Received entries for synchronization");
157
158                let instance = match self.instance() {
159                    Ok(i) => i,
160                    Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
161                };
162
163                // Group entries by tree_id so we can fire callbacks per-database.
164                // BTreeMap so iteration order is deterministic (sorted by id);
165                // sender order within a tree is preserved by per-tree push order.
166                //
167                // Root entries declare an empty `tree.root` and act as their
168                // own tree_id. Non-root entries always carry a tree_id;
169                // well-formed peers should never send a non-root entry with
170                // `root() == None`. If they do, the entry ends up filed under
171                // its own id and parent-existence checks downstream reject it.
172                let mut by_tree: BTreeMap<ID, Vec<Entry>> = BTreeMap::new();
173                for entry in entries {
174                    let tree_id = entry.root().unwrap_or_else(|| entry.id());
175                    by_tree.entry(tree_id).or_default().push(entry.clone());
176                }
177
178                let mut stored_count = 0usize;
179                for (tree_id, tree_entries) in by_tree {
180                    let batch_size = tree_entries.len();
181                    // Entries arrive over the wire without per-entry signature
182                    // verification; `put_remote_entries` stores them
183                    // Unverified so a future re-verification pass can promote
184                    // them.
185                    match instance.put_remote_entries(&tree_id, tree_entries).await {
186                        Ok(n) => {
187                            stored_count += n;
188                            debug!(tree_id = %tree_id, requested = batch_size, stored = n, "Stored entries");
189                        }
190                        Err(e) => {
191                            error!(tree_id = %tree_id, error = %e, "Failed to store entries batch");
192                        }
193                    }
194                }
195
196                debug!(
197                    received = count,
198                    stored = stored_count,
199                    "Completed entry synchronization"
200                );
201                if count <= 1 {
202                    SyncResponse::Ack
203                } else {
204                    SyncResponse::Count(stored_count)
205                }
206            }
207        }
208    }
209}
210
211impl SyncHandlerImpl {
212    /// Get the highest permission level a key has in the database's auth settings.
213    ///
214    /// This looks up all permissions the key has (direct + global wildcard) and returns
215    /// the highest one. Used for auto-detecting permissions during bootstrap.
216    ///
217    /// # Arguments
218    /// * `tree_id` - The database/tree ID to check auth settings for
219    /// * `requesting_pubkey` - The public key to look up
220    ///
221    /// # Returns
222    /// - `Ok(Some(Permission))` if key has any permissions
223    /// - `Ok(None)` if key not found in auth settings
224    /// - `Err` if database access fails
225    async fn get_key_highest_permission(
226        &self,
227        tree_id: &ID,
228        requesting_pubkey: &PublicKey,
229    ) -> Result<Option<Permission>> {
230        let database = Database::open(&self.instance()?, tree_id).await?;
231        let transaction = database.new_transaction().await?;
232        let settings_store = SettingsStore::new(&transaction)?;
233        let auth_settings = settings_store.auth_snapshot().await?;
234
235        let results = auth_settings.find_all_sigkeys_for_pubkey(requesting_pubkey);
236
237        if results.is_empty() {
238            return Ok(None);
239        }
240
241        // Results are sorted highest first, so take the first one
242        Ok(Some(results[0].1))
243    }
244
245    /// Check if the requesting key already has sufficient permissions through existing auth.
246    ///
247    /// This uses the AuthSettings.can_access() method to check if the requesting key
248    /// already has sufficient permissions (including through global '*' permissions).
249    ///
250    /// # Arguments
251    /// * `tree_id` - The database/tree ID to check auth settings for
252    /// * `requesting_pubkey` - The public key making the request
253    /// * `requested_permission` - The permission level being requested
254    ///
255    /// # Returns
256    /// - `Ok(true)` if key has sufficient permission
257    /// - `Ok(false)` if key lacks sufficient permission or auth check fails
258    async fn check_existing_auth_permission(
259        &self,
260        tree_id: &ID,
261        requesting_pubkey: &PublicKey,
262        requested_permission: &Permission,
263    ) -> Result<bool> {
264        let database = Database::open(&self.instance()?, tree_id).await?;
265        let settings_store = database.get_settings().await?;
266
267        let auth_settings = settings_store.auth_snapshot().await?;
268
269        // Use the AuthSettings.can_access() method to check permissions
270        if auth_settings.can_access(requesting_pubkey, requested_permission) {
271            debug!(
272                tree_id = %tree_id,
273                requesting_pubkey = %requesting_pubkey,
274                requested_permission = ?requested_permission,
275                "Key has sufficient permission for bootstrap access"
276            );
277            return Ok(true);
278        }
279
280        Ok(false)
281    }
282
283    /// Check if a database requires authentication for unauthenticated requests.
284    ///
285    /// This method checks if the database requires authentication for bootstrap requests
286    /// that don't provide credentials. A database allows unauthenticated access if:
287    /// 1. It has no auth settings configured at all (empty auth), OR
288    /// 2. It has a global `*` permission configured that allows unauthenticated access
289    ///
290    /// # Arguments
291    /// * `tree_id` - The database/tree ID to check auth configuration for
292    ///
293    /// # Returns
294    /// - `Ok(true)` if database requires authentication (has auth but no global permission)
295    /// - `Ok(false)` if database allows unauthenticated access (no auth or has global permission)
296    /// - `Err` if the check fails
297    async fn check_if_database_has_auth(&self, tree_id: &ID) -> Result<bool> {
298        let database = Database::open(&self.instance()?, tree_id).await?;
299        let transaction = database.new_transaction().await?;
300        let settings_store = SettingsStore::new(&transaction)?;
301
302        let auth_settings = settings_store.auth_snapshot().await?;
303
304        // Check if auth settings is completely empty (no auth configured)
305        if auth_settings.as_doc().is_empty() {
306            debug!(
307                tree_id = %tree_id,
308                "Database has no auth configured - allowing unauthenticated access"
309            );
310            return Ok(false); // No auth required
311        }
312
313        // Auth is configured - check if there's an Active global permission
314        if let Ok(global_key) = auth_settings.get_global_key()
315            && *global_key.status() == KeyStatus::Active
316        {
317            debug!(
318                tree_id = %tree_id,
319                global_permission = ?global_key.permissions(),
320                "Database has global permission - allowing unauthenticated access"
321            );
322            return Ok(false); // Global permission allows unauthenticated access
323        }
324
325        // Auth is configured but no global permission - require authentication
326        debug!(
327            tree_id = %tree_id,
328            "Database has auth configured without global permission - requiring authentication"
329        );
330        Ok(true) // Auth required
331    }
332
333    /// Check if a database has sync enabled by at least one user.
334    ///
335    /// This is a security-critical check that determines if a database should accept
336    /// any sync requests at all. A database is only eligible for sync if at least one
337    /// user has it in their preferences with `sync_enabled: true`.
338    ///
339    /// # Security
340    /// This method implements fail-closed behavior:
341    /// - Returns `false` on any error (no information leakage)
342    /// - Returns `false` if no users have the database in preferences
343    /// - Returns `false` if combined_settings.sync_enabled is false
344    /// - Only returns `true` if explicitly enabled
345    ///
346    /// # Arguments
347    /// * `tree_id` - The ID of the database to check
348    ///
349    /// # Returns
350    /// `true` if the database has sync enabled, `false` otherwise (including errors)
351    async fn is_database_sync_enabled(&self, tree_id: &ID) -> bool {
352        let instance = match self.instance() {
353            Ok(i) => i,
354            Err(_) => return false, // Fail closed
355        };
356
357        let signing_key = match instance.signing_key() {
358            Ok(k) => k.clone(),
359            Err(_) => return false, // Fail closed
360        };
361
362        let sync_database = match Database::open(&instance, &self.sync_tree_id).await {
363            Ok(db) => db.with_key(signing_key),
364            Err(_) => return false, // Fail closed
365        };
366
367        let transaction = match sync_database.new_transaction().await {
368            Ok(tx) => tx,
369            Err(_) => return false, // Fail closed
370        };
371
372        // Use UserSyncManager to get combined settings
373        let user_mgr = UserSyncManager::new(&transaction);
374        match user_mgr.get_combined_settings(tree_id).await {
375            Ok(Some(settings)) => settings.sync_enabled,
376            _ => false, // Fail closed: no settings or error
377        }
378    }
379
380    /// Register an incoming peer and add their addresses to the peer list.
381    ///
382    /// This method registers a peer that initiated a connection to us during handshake.
383    /// It adds both the peer-advertised addresses and the transport-provided remote address.
384    ///
385    /// # Arguments
386    /// * `peer_pubkey` - The peer's public key
387    /// * `display_name` - Optional display name for the peer
388    /// * `advertised_addresses` - Addresses the peer advertised in their handshake
389    /// * `remote_address` - The actual address from which the connection originated
390    ///
391    /// # Returns
392    /// Result indicating success or failure of registration
393    async fn register_incoming_peer(
394        &self,
395        peer_pubkey: &PublicKey,
396        display_name: Option<&str>,
397        advertised_addresses: &[Address],
398        remote_address: &Option<Address>,
399    ) -> Result<()> {
400        let sync_tree = self.get_sync_tree().await?;
401        let txn = sync_tree.new_transaction().await?;
402        let peer_manager = PeerManager::new(&txn);
403
404        // Try to register the peer (ignore if already exists)
405        match peer_manager.register_peer(peer_pubkey, display_name).await {
406            Ok(()) => {
407                info!(peer_pubkey = %peer_pubkey, "Registered new incoming peer");
408            }
409            Err(Error::Sync(ref e)) if matches!(**e, SyncError::PeerAlreadyExists(_)) => {
410                debug!(peer_pubkey = %peer_pubkey, "Peer already registered, updating addresses");
411            }
412            Err(e) => return Err(e),
413        }
414
415        // Add all advertised addresses
416        for addr in advertised_addresses {
417            if let Err(e) = peer_manager.add_address(peer_pubkey, addr.clone()).await {
418                warn!(peer_pubkey = %peer_pubkey, address = ?addr, error = %e, "Failed to add advertised address");
419            }
420        }
421
422        // Add the remote address from transport if available
423        if let Some(addr) = remote_address
424            && let Err(e) = peer_manager.add_address(peer_pubkey, addr.clone()).await
425        {
426            warn!(peer_pubkey = %peer_pubkey, address = ?addr, error = %e, "Failed to add remote address");
427        }
428
429        txn.commit().await?;
430        Ok(())
431    }
432
433    /// Track tree/peer sync relationship when a peer requests a tree.
434    ///
435    /// This method adds the tree to the peer's sync list, enabling bidirectional
436    /// sync for the requested tree. This is critical for `sync_on_commit` to work
437    /// in both directions.
438    ///
439    /// # Arguments
440    /// * `tree_id` - The ID of the tree being requested
441    /// * `peer_pubkey` - The public key of the peer requesting the tree (device key, not auth key)
442    ///
443    /// # Returns
444    /// Result indicating success or failure
445    async fn track_tree_sync_relationship(
446        &self,
447        tree_id: &ID,
448        peer_pubkey: &PublicKey,
449    ) -> Result<()> {
450        let sync_tree = self.get_sync_tree().await?;
451        let txn = sync_tree.new_transaction().await?;
452        let peer_manager = PeerManager::new(&txn);
453
454        // Add the tree sync relationship
455        peer_manager.add_tree_sync(peer_pubkey, tree_id).await?;
456        txn.commit().await?;
457
458        debug!(tree_id = %tree_id, peer_pubkey = %peer_pubkey, "Tracked tree/peer sync relationship");
459        Ok(())
460    }
461
462    /// Handle a handshake request from a peer.
463    async fn handle_handshake(
464        &self,
465        request: &HandshakeRequest,
466        context: &RequestContext,
467    ) -> SyncResponse {
468        async move {
469            debug!(
470                peer_device_id = %request.device_id,
471                peer_public_key = %request.public_key,
472                display_name = ?request.display_name,
473                protocol_version = request.protocol_version,
474                "Processing handshake request"
475            );
476
477            // Check protocol version compatibility
478            if request.protocol_version != PROTOCOL_VERSION {
479                warn!(
480                    expected = PROTOCOL_VERSION,
481                    received = request.protocol_version,
482                    "Protocol version mismatch"
483                );
484                return SyncResponse::Error(format!(
485                    "Protocol version mismatch: expected {}, got {}",
486                    PROTOCOL_VERSION, request.protocol_version
487                ));
488            }
489
490            // Get device signing key from backend
491            let instance = match self.instance() {
492                Ok(i) => i,
493                Err(e) => {
494                    error!(error = %e, "Failed to get instance");
495                    return SyncResponse::Error(format!("Failed to get instance: {e}"));
496                }
497            };
498            let signing_key = match instance.signing_key() {
499                Ok(k) => k.clone(),
500                Err(e) => {
501                    error!(error = %e, "Failed to get device key");
502                    return SyncResponse::Error(format!("Failed to get device key: {e}"));
503                }
504            };
505
506            // Generate device ID and public key from signing key
507            let public_key = signing_key.public_key();
508            let device_id = public_key.clone(); // Device ID is the public key
509
510            // Sign the challenge with our device key to prove identity
511            let challenge_response = create_challenge_response(&request.challenge, &signing_key);
512
513            // Generate a new challenge for mutual authentication
514            let new_challenge = generate_challenge();
515
516            // Get available trees for discovery
517            let available_trees = self.get_available_trees().await;
518
519            // Register the peer and add their addresses to our peer list
520            match self.register_incoming_peer(&request.public_key, request.display_name.as_deref(), &request.listen_addresses, &context.remote_address).await {
521                Ok(()) => {
522                    debug!(peer_pubkey = %request.public_key, "Successfully registered incoming peer");
523                }
524                Err(e) => {
525                    // Log the error but don't fail the handshake - peer registration is best-effort
526                    warn!(peer_pubkey = %request.public_key, error = %e, "Failed to register incoming peer");
527                }
528            }
529
530            info!(
531                our_device_id = %device_id,
532                peer_device_id = %request.device_id,
533                tree_count = available_trees.len(),
534                "Handshake completed successfully"
535            );
536
537            SyncResponse::Handshake(HandshakeResponse {
538                device_id,
539                public_key,
540                display_name: Some("Eidetica Peer".to_string()),
541                protocol_version: PROTOCOL_VERSION,
542                challenge_response,
543                new_challenge,
544                available_trees,
545            })
546        }
547        .instrument(info_span!("handle_handshake", peer = %request.device_id))
548        .await
549    }
550
551    /// Handle a unified sync tree request (bootstrap or incremental).
552    ///
553    /// This method routes between two sync modes:
554    /// 1. **Bootstrap**: When peer has no tips (empty database), sends complete tree
555    /// 2. **Incremental**: When peer has existing tips, sends only new entries
556    ///
557    /// # Bootstrap Authentication
558    /// During bootstrap, if the peer provides authentication credentials:
559    /// - `requesting_key`: Public key to add
560    /// - `requesting_key_name`: Name for the key
561    /// - `requested_permission`: Access level requested
562    ///
563    /// The handler will evaluate the bootstrap policy and either:
564    /// - Auto-approve and add the key immediately
565    /// - Store request for manual approval
566    /// - Proceed without authentication (anonymous bootstrap)
567    async fn handle_sync_tree(
568        &self,
569        request: &SyncTreeRequest,
570        context: &RequestContext,
571    ) -> SyncResponse {
572        async move {
573            trace!(tree_id = %request.tree_id, "Processing sync tree request");
574
575            // Track tree/peer sync relationship for bidirectional sync
576            // IMPORTANT: Only use context.peer_pubkey (device key from handshake)
577            // Do NOT use request.requesting_key (that's an auth key for database access)
578            if let Some(peer_pubkey) = &context.peer_pubkey {
579                if let Err(e) = self.track_tree_sync_relationship(&request.tree_id, peer_pubkey).await {
580                    // Log the error but don't fail the sync - relationship tracking is best-effort
581                    warn!(tree_id = %request.tree_id, peer_pubkey = %peer_pubkey, error = %e, "Failed to track tree/peer relationship");
582                }
583            } else {
584                debug!(tree_id = %request.tree_id, "No peer pubkey in context, skipping relationship tracking");
585            }
586
587            // Check if peer needs bootstrap (empty tips indicates no local data)
588            if request.our_tips.is_empty() {
589                debug!(tree_id = %request.tree_id, "Peer needs bootstrap - sending full tree");
590                return self.handle_bootstrap_request(&request.tree_id,
591                                                  request.requesting_key.as_ref(),
592                                                  request.requesting_key_name.as_deref(),
593                                                  request.requested_permission).await;
594            }
595
596            // Handle incremental sync (peer has existing data, needs updates)
597            debug!(tree_id = %request.tree_id, peer_tips = request.our_tips.len(), "Handling incremental sync");
598            self.handle_incremental_sync(&request.tree_id, &request.our_tips).await
599        }
600        .instrument(info_span!("handle_sync_tree", tree = %request.tree_id))
601        .await
602    }
603
604    /// Handle bootstrap request by sending complete tree state and optionally approving auth key.
605    ///
606    /// Bootstrap is the initial synchronization when a peer has no local data for a tree.
607    /// This method:
608    /// 1. Validates the tree exists and sync is enabled
609    /// 2. Processes authentication and permission resolution
610    /// 3. Sends all entries from the tree to the peer
611    ///
612    /// # Authentication Flow
613    ///
614    /// The bootstrap process handles three authentication scenarios:
615    ///
616    /// ## 1. Explicit Permission Request
617    /// When all three auth parameters are provided (`requesting_key`, `requesting_key_name`, `requested_permission`):
618    /// - Check if key already has sufficient permissions
619    /// - If yes: Approve immediately without adding key
620    /// - If no: Store request for manual approval and return `BootstrapPending`
621    ///
622    /// ## 2. Auto-Detection
623    /// When key is provided but `requested_permission` is `None`:
624    /// - Look up key's existing permissions in database auth settings
625    /// - Uses `find_all_sigkeys_for_pubkey()` to find all permissions (direct + global wildcard)
626    /// - If key found: Use highest available permission and approve immediately
627    /// - If key not found: Reject with authentication error
628    ///
629    /// ## 3. Unauthenticated Access
630    /// When no `requesting_key` is provided:
631    /// - Only allowed if database has no auth configured or has global wildcard permission
632    /// - Otherwise rejected with authentication required error
633    ///
634    /// # Note on Key Verification
635    ///
636    /// This function does not verify that the peer actually controls the `requesting_key`.
637    /// The `requesting_key` parameter is an unverified string from the client.
638    ///
639    /// **This is not a security vulnerability** because:
640    /// - Approval only adds the public key to database auth settings
641    /// - Actual database access requires signing entries with the corresponding private key
642    /// - If an attacker claims someone else's public key, approval grants access to the
643    ///   legitimate key holder (who has the private key), not the attacker
644    ///
645    /// The lack of verification may cause:
646    /// - Audit trail confusion (request appears to come from a different identity)
647    /// - Admins approving access for keys that didn't actually request it
648    ///
649    /// # Arguments
650    /// * `tree_id` - The database/tree to bootstrap
651    /// * `requesting_key` - Optional public key requesting access (unverified, but safe - see above)
652    /// * `requesting_key_name` - Optional name/identifier for the key (unverified)
653    /// * `requested_permission` - Optional permission level requested (if None, auto-detects from auth settings)
654    ///
655    /// # Returns
656    /// - `BootstrapResponse`: Contains entries and approval status (key_approved, granted_permission)
657    /// - `BootstrapPending`: Manual approval required (request queued)
658    /// - `Error`: Tree not found, auth required, key not authorized, or processing failure
659    async fn handle_bootstrap_request(
660        &self,
661        tree_id: &ID,
662        requesting_key: Option<&PublicKey>,
663        requesting_key_name: Option<&str>,
664        requested_permission: Option<Permission>,
665    ) -> SyncResponse {
666        // SECURITY: Check if database has sync enabled (FIRST CHECK - before anything else)
667        // This prevents information leakage about database existence: the gate
668        // returns false both for databases that are absent and for databases that
669        // are present-but-not-tracked-for-sync, and we deliberately respond with
670        // the same opaque "Tree not found" to peers in either case.
671        if !self.is_database_sync_enabled(tree_id).await {
672            warn!(
673                tree_id = %tree_id,
674                requesting_key = ?requesting_key,
675                requesting_key_name = ?requesting_key_name,
676                "Bootstrap request rejected: database is absent or has no user with sync enabled (responding as not-found)"
677            );
678            return SyncResponse::Error(format!("Tree not found: {tree_id}"));
679        }
680
681        // Get the root entry (to verify tree exists)
682        let instance = match self.instance() {
683            Ok(i) => i,
684            Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
685        };
686        let _root_entry = match instance.backend().get(tree_id).await {
687            Ok(entry) => entry,
688            Err(e) if e.is_not_found() => {
689                warn!(
690                    tree_id = %tree_id,
691                    requesting_key = ?requesting_key,
692                    requesting_key_name = ?requesting_key_name,
693                    "Bootstrap request rejected: a user has this tree marked sync-enabled but the backend has no root entry for it"
694                );
695                return SyncResponse::Error(format!("Tree not found: {tree_id}"));
696            }
697            Err(e) => {
698                error!(tree_id = %tree_id, error = %e, "Failed to get root entry");
699                return SyncResponse::Error(format!("Failed to get tree root: {e}"));
700            }
701        };
702
703        // Check if database has authentication configured
704        let auth_configured = match self.check_if_database_has_auth(tree_id).await {
705            Ok(has_auth) => has_auth,
706            Err(e) => {
707                error!(tree_id = %tree_id, error = %e, "Failed to check if database has auth");
708                return SyncResponse::Error(format!("Failed to check database auth: {e}"));
709            }
710        };
711
712        // If auth is configured but no credentials provided, reject the request
713        if auth_configured && requesting_key.is_none() {
714            warn!(
715                tree_id = %tree_id,
716                "Unauthenticated bootstrap request rejected - database requires authentication"
717            );
718            return SyncResponse::Error(
719                "Authentication required: This database requires authenticated access. \
720                 Please provide credentials (requesting_key, requesting_key_name, requested_permission) \
721                 to bootstrap sync.".to_string()
722            );
723        }
724
725        // Handle key approval for bootstrap requests FIRST
726        let (key_approved, granted_permission) = match (
727            requesting_key,
728            requesting_key_name,
729            requested_permission,
730        ) {
731            // Case 1: All three parameters provided - explicit permission request
732            (Some(key), Some(key_name), Some(permission)) => {
733                info!(
734                    tree_id = %tree_id,
735                    requesting_key = %key,
736                    key_name = %key_name,
737                    requested_permission = ?permission,
738                    "Processing key approval request for bootstrap"
739                );
740
741                // Check if the requesting key already has sufficient permissions through existing auth
742                match self
743                    .check_existing_auth_permission(tree_id, key, &permission)
744                    .await
745                {
746                    Ok(true) => {
747                        // Key already has sufficient permission - approve without adding
748                        info!(
749                            tree_id = %tree_id,
750                            key = %key,
751                            permission = ?permission,
752                            "Bootstrap approved via existing auth permission - no key added"
753                        );
754                        (true, Some(permission))
755                    }
756                    Ok(false) => {
757                        // No existing permission, store request for manual approval
758                        info!(tree_id = %tree_id, "Bootstrap key approval requested - storing for manual approval");
759
760                        // Store the bootstrap request in sync database for manual approval
761                        match self
762                            .store_bootstrap_request(tree_id, key, key_name, &permission)
763                            .await
764                        {
765                            Ok(request_id) => {
766                                info!(
767                                    tree_id = %tree_id,
768                                    request_id = %request_id,
769                                    "Bootstrap request stored for manual approval"
770                                );
771                                return SyncResponse::BootstrapPending {
772                                    request_id,
773                                    message: "Bootstrap request pending manual approval"
774                                        .to_string(),
775                                };
776                            }
777                            Err(e) => {
778                                error!(
779                                    tree_id = %tree_id,
780                                    error = %e,
781                                    "Failed to store bootstrap request"
782                                );
783                                return SyncResponse::Error(format!(
784                                    "Failed to store bootstrap request: {e}"
785                                ));
786                            }
787                        }
788                    }
789                    Err(e) => {
790                        error!(tree_id = %tree_id, error = %e, "Failed to check global permission for bootstrap");
791                        return SyncResponse::Error(format!("Global permission check failed: {e}"));
792                    }
793                }
794            }
795
796            // Case 2: Key provided but permission not specified - auto-detect from auth settings
797            (Some(key), Some(_key_name), None) => {
798                info!(
799                    tree_id = %tree_id,
800                    requesting_key = %key,
801                    "Auto-detecting permission from auth settings for bootstrap request"
802                );
803
804                match self.get_key_highest_permission(tree_id, key).await {
805                    Ok(Some(permission)) => {
806                        info!(
807                            tree_id = %tree_id,
808                            requesting_key = %key,
809                            detected_permission = ?permission,
810                            "Approved bootstrap using auto-detected permission from auth settings"
811                        );
812                        (true, Some(permission))
813                    }
814                    Ok(None) => {
815                        warn!(
816                            tree_id = %tree_id,
817                            requesting_key = %key,
818                            "Key not found in auth settings - rejecting bootstrap request"
819                        );
820                        return SyncResponse::Error(
821                            "Authentication required: provided key is not authorized for this database".to_string()
822                        );
823                    }
824                    Err(e) => {
825                        error!(
826                            tree_id = %tree_id,
827                            requesting_key = %key,
828                            error = %e,
829                            "Failed to lookup key permissions"
830                        );
831                        return SyncResponse::Error(format!("Failed to access auth settings: {e}"));
832                    }
833                }
834            }
835
836            // Case 3: No key provided, or key provided without key_name - unauthenticated access
837            _ => {
838                debug!(
839                    tree_id = %tree_id,
840                    "No authentication credentials provided - proceeding with unauthenticated bootstrap"
841                );
842                (false, None)
843            }
844        };
845
846        // NOW collect all entries after key approval (so we get the updated database state)
847        let all_entries = match self.collect_all_entries_for_bootstrap(tree_id).await {
848            Ok(entries) => entries,
849            Err(e) => {
850                error!(tree_id = %tree_id, error = %e, "Failed to collect all entries for bootstrap after key approval");
851                return SyncResponse::Error(format!(
852                    "Failed to collect all entries for bootstrap: {e}"
853                ));
854            }
855        };
856
857        // For bootstrap, we need to send the actual root entry (tree_id) as root_entry
858        // The root_entry should always be the tree's root, not a tip
859        let instance = match self.instance() {
860            Ok(i) => i,
861            Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
862        };
863        let root_entry = match instance.backend().get(tree_id).await {
864            Ok(entry) => entry,
865            Err(e) => {
866                error!(tree_id = %tree_id, error = %e, "Failed to get root entry");
867                return SyncResponse::Error(format!("Failed to get root entry: {e}"));
868            }
869        };
870
871        // Filter out the root from all_entries since we send it separately as root_entry
872        let other_entries: Vec<_> = all_entries
873            .into_iter()
874            .filter(|entry| entry.id() != *tree_id)
875            .collect();
876
877        info!(
878            tree_id = %tree_id,
879            entry_count = other_entries.len() + 1,
880            key_approved = key_approved,
881            "Sending bootstrap response"
882        );
883
884        SyncResponse::Bootstrap(BootstrapResponse {
885            tree_id: tree_id.clone(),
886            root_entry,
887            all_entries: other_entries,
888            key_approved,
889            granted_permission,
890        })
891    }
892
893    /// Handle incremental sync request
894    async fn handle_incremental_sync(&self, tree_id: &ID, peer_tips: &[ID]) -> SyncResponse {
895        // SECURITY: Check if database has sync enabled (FIRST CHECK - before anything else)
896        // This prevents information leakage about database existence: the gate
897        // returns false both for databases that are absent and for databases that
898        // are present-but-not-tracked-for-sync, and we deliberately respond with
899        // the same opaque "Tree not found" to peers in either case.
900        if !self.is_database_sync_enabled(tree_id).await {
901            warn!(
902                tree_id = %tree_id,
903                peer_tip_count = peer_tips.len(),
904                "Incremental sync request rejected: database is absent or has no user with sync enabled (responding as not-found)"
905            );
906            return SyncResponse::Error(format!("Tree not found: {tree_id}"));
907        }
908
909        // Get our current tips
910        let instance = match self.instance() {
911            Ok(i) => i,
912            Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
913        };
914        let our_tips: Vec<ID> = match instance.backend().snapshot(tree_id).await {
915            Ok(snap) => snap.into_tips(),
916            Err(e) => {
917                error!(tree_id = %tree_id, error = %e, "Failed to get our tips");
918                return SyncResponse::Error(format!("Failed to get tips: {e}"));
919            }
920        };
921
922        // Find entries peer is missing
923        let missing_entries = match self
924            .find_missing_entries_for_peer(&our_tips, peer_tips)
925            .await
926        {
927            Ok(entries) => entries,
928            Err(e) => {
929                error!(tree_id = %tree_id, error = %e, "Failed to find missing entries");
930                return SyncResponse::Error(format!("Failed to find missing entries: {e}"));
931            }
932        };
933
934        debug!(
935            tree_id = %tree_id,
936            our_tips = our_tips.len(),
937            peer_tips = peer_tips.len(),
938            missing_count = missing_entries.len(),
939            "Sending incremental sync response"
940        );
941
942        SyncResponse::Incremental(IncrementalResponse {
943            tree_id: tree_id.clone(),
944            their_tips: our_tips,
945            missing_entries,
946        })
947    }
948}