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

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