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

eidetica/service/
server.rs

1//! Service server: accepts Unix socket connections and dispatches `BackendImpl` operations.
2//!
3//! The server wraps an `Instance` (not just a backend) so it can handle write
4//! notifications through the Instance's callback system.
5
6use std::collections::{HashMap, HashSet};
7use std::os::unix::fs::PermissionsExt;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use tokio::net::UnixListener;
13use tokio::sync::{mpsc, watch};
14use tokio::task::JoinSet;
15
16use crate::Instance;
17use crate::auth::crypto::{PublicKey, generate_challenge, verify_challenge_response};
18use crate::auth::errors::AuthError;
19use crate::auth::types::{Permission, SigKey};
20use crate::auth::validation::permissions::resolve_identity_permission;
21use crate::backend::{CacheScope, VerificationStatus};
22use crate::database::Database;
23use crate::entry::ID;
24use crate::instance::{CallbackId, WriteSource};
25use crate::service::error::ServiceError;
26use crate::service::protocol::{
27    AuthenticatedDbRequest, DatabaseOp, HandshakeAck, MergeState, Notification, PROTOCOL_VERSION,
28    ServerFrame, ServiceRequest, ServiceResponse, read_frame, write_frame,
29};
30use crate::user::system_databases::lookup_user_record;
31
32/// Connection identifier. Monotonic per server-run; reused only after
33/// `AtomicU64` wraps (effectively never on a real daemon). Purely
34/// diagnostic now — registry-based routing went away in the per-db
35/// callback refactor.
36type ConnectionId = u64;
37
38/// Per-connection context carried through the dispatch chain. Holds:
39///
40/// - `tx`: the writer-channel sender — both `ServerFrame::Response`s from
41///   the dispatcher and `ServerFrame::Notification`s from subscribed
42///   per-db callbacks ride through this same channel, so the order
43///   observed by the client is the order frames hit `frame_tx`.
44/// - `instance`: needed in `Drop` to call `remove_write_callback` for
45///   the connection's registered subscriptions.
46/// - `subscribed`: `root_id -> CallbackId` for every `SubscribeWrites`
47///   this connection has done. Cleaned up on disconnect (via the guard's
48///   `Drop`) and on explicit `UnsubscribeWrites`.
49///
50/// There is no per-connection registry on the server. Each subscription
51/// is just a per-db callback registered against the daemon's Instance
52/// (`Instance::register_write_callback`); the daemon's existing
53/// `fire_write_callbacks` dispatch path handles fan-out by walking the
54/// per-tree callback list, no separate fan-out mechanism required.
55struct ConnectionContext {
56    conn_id: ConnectionId,
57    tx: mpsc::UnboundedSender<ServerFrame>,
58    instance: Instance,
59    subscribed: std::sync::Mutex<HashMap<ID, CallbackId>>,
60}
61
62impl ConnectionContext {
63    fn subscribed_lock(&self) -> std::sync::MutexGuard<'_, HashMap<ID, CallbackId>> {
64        self.subscribed
65            .lock()
66            .unwrap_or_else(|poisoned| poisoned.into_inner())
67    }
68}
69
70/// RAII cleanup: on connection drop (clean EOF, error, or panic), call
71/// `Instance::remove_write_callback` for every subscription this
72/// connection registered. Holds an `Arc<ConnectionContext>` rather than
73/// borrowing so the cleanup path is identical for every exit case
74/// without sprinkling `remove_*` calls through the connection handler.
75struct ConnectionGuard {
76    ctx: Arc<ConnectionContext>,
77}
78
79impl Drop for ConnectionGuard {
80    fn drop(&mut self) {
81        let subs = self.ctx.subscribed_lock();
82        if !subs.is_empty() {
83            tracing::debug!(
84                conn_id = self.ctx.conn_id,
85                "Unregistering {} subscriptions on disconnect",
86                subs.len()
87            );
88        }
89        for (tree_id, id) in subs.iter() {
90            self.ctx.instance.remove_write_callback(tree_id, *id);
91        }
92    }
93}
94
95/// Per-connection authentication state.
96///
97/// A connection moves `PreAuth → AwaitingProof → Authenticated` on a successful
98/// `TrustedLoginUser` / `TrustedLoginProve` exchange. A failed proof drops the
99/// connection back to `PreAuth` so the client can retry without reconnecting.
100/// Any other request while in `AwaitingProof` also resets the state so a
101/// half-finished login can't be exploited mid-flight.
102///
103/// "Trusted" refers to the assumption that whoever can reach this socket is
104/// already authorised by filesystem permissions (mode 0600 under
105/// `$XDG_RUNTIME_DIR`); see the protocol module docs and the Service
106/// Architecture brain doc § Trusted login threat model.
107#[derive(Debug, Clone)]
108enum ConnectionState {
109    /// No login attempt yet, or last attempt failed/abandoned.
110    PreAuth,
111    /// `TrustedLoginUser` succeeded; waiting for the client's `TrustedLoginProve`.
112    AwaitingProof {
113        username: String,
114        user_uuid: String,
115        challenge: Vec<u8>,
116        expected_pubkey: PublicKey,
117    },
118    /// Login completed. `login_pubkey` is the verified root pubkey for the
119    /// user, established at `TrustedLoginProve` time. `session_keyset` is the
120    /// set of pubkeys the client has further proven possession of via
121    /// `SessionKeyChallenge`/`SessionKeyRegister`; it always contains
122    /// `login_pubkey` and may include additional per-DB keys the user owns.
123    /// The dispatch path for `Authenticated`/`AuthenticatedDb` requests
124    /// validates the identity hint against this set and gates against the
125    /// resulting *acting* pubkey, so a single connection can drive ops on
126    /// databases authored by any key the user has proven they hold.
127    /// `user_uuid` is the per-user scope key for the unified CRDT-state
128    /// cache (see [`crate::backend::CacheScope::User`]).
129    /// `pending_key_challenges` holds outstanding registration challenges,
130    /// keyed by the pubkey the challenge was issued for. Each challenge is
131    /// single-use: the matching `SessionKeyRegister` consumes it whether
132    /// verification succeeds or fails.
133    #[allow(dead_code)] // username surfaces in audit/logging follow-ups
134    Authenticated {
135        username: String,
136        user_uuid: String,
137        login_pubkey: PublicKey,
138        session_keyset: HashSet<PublicKey>,
139        pending_key_challenges: HashMap<PublicKey, Vec<u8>>,
140    },
141}
142
143/// Eidetica service server that listens on a Unix domain socket.
144///
145/// The server wraps a full `Instance` so it can dispatch both storage operations
146/// (via the backend) and write callbacks (via `Instance::put_entry()`'s notification path).
147///
148/// CRDT-state caching lives in the underlying `BackendImpl` (scope-keyed
149/// via [`crate::backend::CacheScope`]); wire handlers route through
150/// `instance.backend()` directly rather than keeping a separate
151/// service-layer cache.
152pub struct ServiceServer {
153    instance: Instance,
154    socket_path: PathBuf,
155}
156
157impl ServiceServer {
158    /// Create a new service server.
159    ///
160    /// # Arguments
161    /// * `instance` - The Instance to serve. The server holds a strong reference.
162    /// * `socket_path` - Path for the Unix domain socket.
163    pub fn new(instance: Instance, socket_path: impl Into<PathBuf>) -> Self {
164        Self {
165            instance,
166            socket_path: socket_path.into(),
167        }
168    }
169
170    /// Get the socket path.
171    pub fn socket_path(&self) -> &Path {
172        &self.socket_path
173    }
174
175    /// Run the server until the shutdown signal is received.
176    ///
177    /// Removes any stale socket file, creates the parent directory, binds the
178    /// listener, and loops accepting connections. Each connection is handled in
179    /// a spawned task. On shutdown, the socket file is cleaned up.
180    ///
181    /// # Arguments
182    /// * `shutdown` - A watch receiver; the server stops when the sender is dropped.
183    pub async fn run(&self, mut shutdown: watch::Receiver<()>) -> crate::Result<()> {
184        // Remove stale socket if it exists
185        if self.socket_path.exists() {
186            tokio::fs::remove_file(&self.socket_path).await?;
187        }
188
189        // Create parent directory with owner-only permissions (0700)
190        if let Some(parent) = self.socket_path.parent() {
191            tokio::fs::create_dir_all(parent).await?;
192            tokio::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)).await?;
193        }
194
195        let listener = UnixListener::bind(&self.socket_path)?;
196
197        // Restrict socket to owner-only access (0600)
198        tokio::fs::set_permissions(&self.socket_path, std::fs::Permissions::from_mode(0o600))
199            .await?;
200
201        tracing::info!("Service server listening on {}", self.socket_path.display());
202
203        // Diagnostic per-connection counter — purely for logging. No
204        // registry-based routing needed; each connection's subscriptions
205        // live as per-db callbacks on the Instance, dispatched by the
206        // existing `fire_write_callbacks` path.
207        let next_conn_id = Arc::new(AtomicU64::new(1));
208
209        // Track active per-connection tasks so shutdown actually
210        // disconnects them. Without this the shutdown signal only stops
211        // the accept loop; live handlers keep running until the client
212        // closes its end, which means "shutdown" is half-done — clients
213        // hang on response reads from a daemon they were told had stopped.
214        let mut handlers = JoinSet::new();
215
216        loop {
217            tokio::select! {
218                accept_result = listener.accept() => {
219                    match accept_result {
220                        Ok((stream, _addr)) => {
221                            let instance = self.instance.clone();
222                            let conn_id = next_conn_id.fetch_add(1, Ordering::Relaxed);
223                            handlers.spawn(async move {
224                                if let Err(e) = handle_connection(stream, instance, conn_id).await {
225                                    tracing::debug!(conn_id, "Connection handler error: {e}");
226                                }
227                            });
228                        }
229                        Err(e) => {
230                            tracing::error!("Failed to accept connection: {e}");
231                        }
232                    }
233                }
234                // Reap finished handlers as they complete. Without this arm
235                // the JoinSet only drains at shutdown, so every connection the
236                // daemon has ever served keeps its task entry alive for the
237                // daemon's lifetime.
238                _ = handlers.join_next(), if !handlers.is_empty() => {}
239                _ = shutdown.changed() => {
240                    tracing::info!("Service server shutting down");
241                    break;
242                }
243            }
244        }
245
246        // Abort live handlers. Each aborted task drops its `UnixStream`
247        // halves, which the client observes as a clean EOF on its read
248        // side — the reader task exits, sets `closed`, and any in-flight
249        // or subsequent `request()` surfaces `ConnectionAborted`.
250        handlers.abort_all();
251        // Drain to completion so the tempdir-owned socket file isn't
252        // pulled out from under any handler that hasn't yet observed
253        // the abort.
254        while handlers.join_next().await.is_some() {}
255
256        // Clean up socket file
257        let _ = tokio::fs::remove_file(&self.socket_path).await;
258        Ok(())
259    }
260}
261
262/// Handle a single client connection.
263///
264/// I/O is split across two tasks:
265///
266/// - **Writer task** (spawned below) owns the `WriteHalf` and drains an
267///   `mpsc::UnboundedReceiver<ServerFrame>` into `write_frame` in
268///   submission order. Responses (dispatched from the request loop) and
269///   server-pushed notifications (from subscribed per-db callbacks both
270///   capture clones of the same `frame_tx`, so a single connection-local
271///   order is preserved.
272///
273/// - **This task** owns the `ReadHalf`, runs the auth/request loop, and
274///   sends `ServerFrame::Response(...)` into the channel. It also holds
275///   a [`ConnectionGuard`] whose Drop unregisters every subscription this
276///   connection made on every exit path (clean EOF, error, or panic).
277///
278/// Handshake still runs inline on the read side first; the writer task is
279/// not started until the handshake succeeds, so a version-mismatch close
280/// uses the simpler inline writer path.
281async fn handle_connection(
282    stream: tokio::net::UnixStream,
283    instance: Instance,
284    conn_id: ConnectionId,
285) -> crate::Result<()> {
286    let (mut reader, mut writer) = tokio::io::split(stream);
287
288    // 1. Read and validate handshake (inline writer, no channel yet).
289    let handshake: crate::service::protocol::Handshake = match read_frame(&mut reader).await? {
290        Some(h) => h,
291        None => return Ok(()), // Client disconnected before handshake
292    };
293
294    if handshake.protocol_version != PROTOCOL_VERSION {
295        // Send error ack and close
296        let ack = HandshakeAck {
297            protocol_version: PROTOCOL_VERSION,
298        };
299        write_frame(&mut writer, &ack).await?;
300        return Err(crate::Error::Io(std::io::Error::new(
301            std::io::ErrorKind::InvalidData,
302            format!(
303                "Protocol version mismatch: client={}, server={}",
304                handshake.protocol_version, PROTOCOL_VERSION
305            ),
306        )));
307    }
308
309    // Send handshake ack
310    let ack = HandshakeAck {
311        protocol_version: PROTOCOL_VERSION,
312    };
313    write_frame(&mut writer, &ack).await?;
314
315    // 2. Spin up the per-connection writer task.
316    //
317    // TODO(backpressure): this channel is `unbounded`, so a stalled client
318    // reader buffers frames in daemon memory without limit. Acceptable
319    // under the v1 trust posture (single trusted local client over a Unix
320    // socket — an unresponsive reader is treated as a client-side bug, not
321    // an attacker). Before serving multiple or untrusted clients this
322    // needs a bound + drop policy. Complication: this single channel
323    // carries both `Response` and `Notification` frames. Naive bounding
324    // is wrong — dropping a `Response` breaks request/response
325    // correlation. Two viable shapes for the follow-up:
326    //
327    //   a) **Split**: separate `response_tx` (bounded, block-send) from
328    //      `notification_tx` (bounded, drop-oldest); writer `select!`s
329    //      with response priority. Cleanest semantics but touches every
330    //      `frame_tx.clone()` site and the cleanup-ordering comment at
331    //      the end of this function needs migration.
332    //
333    //   b) **Variant-aware drop**: bound the single channel; on full,
334    //      `try_send` returns `Full(frame)`; reject if Notification,
335    //      block if Response. Less plumbing, policy duplicated per send
336    //      site.
337    //
338    // Under the cursor-only `Notification::DatabaseWrite` shape (no entry
339    // bodies, just `previous_tips`/`post_tips`), drops are *recoverable*:
340    // a subscriber that misses event N still sees event N+1's `post_tips`
341    // and calls `ids_added(cursor, post_tips)` to catch up. That makes
342    // drop-oldest the right policy and considerably softens the cost of
343    // dropping at all — but the work still needs doing for prod.
344    let (frame_tx, mut frame_rx) = mpsc::unbounded_channel::<ServerFrame>();
345    let writer_task = tokio::spawn(async move {
346        while let Some(frame) = frame_rx.recv().await {
347            if let Err(e) = write_frame(&mut writer, &frame).await {
348                tracing::debug!(conn_id, "Connection writer error: {e}");
349                break;
350            }
351        }
352    });
353
354    let ctx = Arc::new(ConnectionContext {
355        conn_id,
356        tx: frame_tx.clone(),
357        instance: instance.clone(),
358        subscribed: std::sync::Mutex::new(HashMap::new()),
359    });
360    let guard = ConnectionGuard { ctx: ctx.clone() };
361
362    // 3. Request/response loop with per-connection auth state. Responses
363    //    travel through the writer channel as `ServerFrame::Response`.
364    let mut state = ConnectionState::PreAuth;
365    let loop_result: crate::Result<()> = async {
366        loop {
367            let request: ServiceRequest = match read_frame(&mut reader).await? {
368                Some(req) => req,
369                None => break, // Clean EOF
370            };
371
372            let response = dispatch(&instance, &mut state, &ctx, request).await;
373            if frame_tx
374                .send(ServerFrame::Response(Box::new(response)))
375                .is_err()
376            {
377                // Writer task has exited; nothing more we can send.
378                break;
379            }
380        }
381        Ok(())
382    }
383    .await;
384
385    // Cleanup ordering matters — every clone of `frame_tx` must be
386    // dropped before `writer_task.await` can complete (the task is
387    // waiting on `frame_rx.recv()`, which only returns `None` when no
388    // senders remain):
389    //
390    // 1. Drop the local `frame_tx` (this fn's own clone).
391    // 2. Drop `guard`, which runs `ConnectionGuard::drop` and calls
392    //    `Instance::remove_write_callback(tree_id, id)` for every
393    //    subscription this connection registered. Each removed callback
394    //    Arc drops its captured `tx` clone. (No-op when nothing was
395    //    subscribed — e.g. handshake-then-malformed-frame paths.)
396    // 3. Drop `ctx` — releases `ctx.tx`, the last sender clone held
397    //    on the request-loop side.
398    // 4. The writer task's `recv()` returns `None`; the task exits and
399    //    `writer_task.await` finishes.
400    //
401    // Note: a callback dispatch in flight when step 2 runs holds its
402    // own Arc to the closure (from `fire_write_callbacks`'s snapshot).
403    // It can still send a final notification through its captured `tx`
404    // before that Arc is released; the writer task will drain that
405    // frame before exiting in step 4.
406    drop(frame_tx);
407    drop(guard);
408    drop(ctx);
409    let _ = writer_task.await;
410
411    // Session teardown: subscription cleanup ran via the guard's Drop.
412    // The unified CRDT-state cache lives in `BackendImpl` and is bounded
413    // by its own eviction policy (byte-bounded LRU on the in-memory
414    // backend; disk-bounded on SQL). Per-user cache slots survive
415    // disconnect intentionally so a reconnecting client recovers
416    // materialized state from tier 2 without recomputing from entries.
417
418    loop_result
419}
420
421/// Dispatch a service request to the appropriate Instance/Backend method.
422async fn dispatch(
423    instance: &Instance,
424    state: &mut ConnectionState,
425    ctx: &ConnectionContext,
426    request: ServiceRequest,
427) -> ServiceResponse {
428    match dispatch_inner(instance, state, ctx, request).await {
429        Ok(resp) => resp,
430        Err(e) => ServiceResponse::Error(ServiceError::from(&e)),
431    }
432}
433
434/// Inner dispatch that returns Result for ergonomic error handling.
435async fn dispatch_inner(
436    instance: &Instance,
437    state: &mut ConnectionState,
438    ctx: &ConnectionContext,
439    request: ServiceRequest,
440) -> crate::Result<ServiceResponse> {
441    match request {
442        // === Pre-auth: login handshake ===
443        ServiceRequest::TrustedLoginUser { username } => {
444            handle_trusted_login_user(instance, state, username).await
445        }
446        ServiceRequest::TrustedLoginProve { signature } => {
447            handle_trusted_login_prove(state, &signature)
448        }
449
450        // === Pre-auth: server identity ===
451        ServiceRequest::GetInstanceMetadata => {
452            let metadata = instance.backend().get_instance_metadata().await?;
453            Ok(ServiceResponse::InstanceMetadata(metadata))
454        }
455
456        // === Post-auth: extend the session keyset ===
457        ServiceRequest::SessionKeyChallenge { pubkey } => {
458            handle_session_key_challenge(state, pubkey)
459        }
460        ServiceRequest::SessionKeyRegister { pubkey, signature } => {
461            handle_session_key_register(state, pubkey, &signature)
462        }
463
464        // === Authenticated storage operations ===
465        //
466        // Gate 1: the connection must have completed `TrustedLogin*`. Gate 2:
467        // the per-tree permission gate. Every `DatabaseOp` carries its target
468        // `root_id` explicitly, so the gate is *unconditional* — there is no
469        // tree-less op to fall through it — with two exceptions handled below
470        // (`SubmitSignedEntry`, verification-gated; `SetInstanceMetadata`,
471        // gated against the server-known `_databases`, not the request root).
472        ServiceRequest::AuthenticatedDb(inner) => {
473            let (login_pubkey, keyset_snapshot, session_user_uuid) = match state {
474                ConnectionState::Authenticated {
475                    login_pubkey,
476                    session_keyset,
477                    user_uuid,
478                    ..
479                } => (
480                    login_pubkey.clone(),
481                    session_keyset.clone(),
482                    user_uuid.clone(),
483                ),
484                _ => {
485                    return Err(crate::Error::Auth(Box::new(
486                        AuthError::InvalidAuthConfiguration {
487                            reason: "database operation requires an authenticated connection; \
488                                 complete TrustedLogin* first"
489                                .to_string(),
490                        },
491                    )));
492                }
493            };
494
495            let AuthenticatedDbRequest {
496                root_id,
497                identity,
498                op,
499            } = *inner;
500
501            // Submit is verification-gated, not session-gated.
502            //
503            // Reads are session-gated (confidentiality boundary); submits
504            // are verification-gated (integrity boundary). `SubmitSignedEntry`
505            // requires only an *authenticated* connection (gate 1, the
506            // `ConnectionState::Authenticated` match above, still applies).
507            // Which tree the entry belongs to, and whether its signer may
508            // write that tree, is decided by the server's own verification
509            // pass in the handler (store `Unverified`, then
510            // `Database::open(...).verify()`) against the tree's *real*
511            // pinned auth lineage — not by who holds the socket. An attacker
512            // without a key the tree's auth grants cannot produce a
513            // `Verified` entry, and unverified junk is excluded from every
514            // default read by the frontier cut, so the per-tree session gate
515            // adds no correctness or isolation property here; it only blocks
516            // a legitimate transporter (e.g. an admin session carrying a
517            // user-signed genesis). See the verification-gated-submit design
518            // doc for the full threat analysis.
519            let is_submit = matches!(op, DatabaseOp::SubmitSignedEntry { .. });
520
521            // `SetInstanceMetadata` rewrites the daemon's pointers to its own
522            // system DBs. It is gated against `_databases` (a server-known
523            // tree), not the request's `root_id`, so an instance admin — by
524            // construction a user with Admin on `_databases` via the
525            // first-user bootstrap — is required. Fail closed (require_existing
526            // = true): `_databases` always exists on an initialized daemon and
527            // this is never a creation flow, so the create-flow passthrough
528            // must not apply (it would let any authenticated user rewrite
529            // system-DB pointers if `_databases` were ever unreadable).
530            let is_set_metadata = matches!(op, DatabaseOp::SetInstanceMetadata { .. });
531
532            // Submit accepts any identity hint (admin transports user-signed
533            // entries); every other op resolves an acting pubkey from the
534            // keyset and gates per-tree against it.
535            let acting_pubkey = if is_submit {
536                // Use the hint if it parses as a pubkey (for submit metadata),
537                // otherwise fall back to login_pubkey — submit doesn't gate
538                // on this value.
539                identity
540                    .hint()
541                    .pubkey
542                    .clone()
543                    .unwrap_or_else(|| login_pubkey.clone())
544            } else {
545                resolve_acting_pubkey(&identity, &login_pubkey, &keyset_snapshot)?
546            };
547
548            // Per-tree permission gate. Unconditional for every op *except*
549            // submit (verification in the handler is its boundary) and
550            // set-metadata (gated against `_databases` below). Create-flow
551            // passthrough (false) for the rest, so a not-yet-propagated tree is
552            // waved through and database creation works.
553            if is_set_metadata {
554                gate_tree_permission(
555                    instance,
556                    &acting_pubkey,
557                    &identity,
558                    instance.databases_db_id(),
559                    Permission::Admin(0),
560                    true,
561                )
562                .await?;
563            } else if !is_submit {
564                gate_tree_permission(
565                    instance,
566                    &acting_pubkey,
567                    &identity,
568                    &root_id,
569                    op.required_permission(),
570                    false,
571                )
572                .await?;
573            }
574
575            dispatch_database_op(
576                instance,
577                ctx,
578                &acting_pubkey,
579                &identity,
580                &session_user_uuid,
581                root_id,
582                op,
583            )
584            .await
585        }
586    }
587}
588
589/// Resolve the *acting* pubkey for a session-gated op.
590///
591/// The identity hint, when present, must be in the connection's session
592/// keyset (proof of possession registered via `SessionKeyChallenge` /
593/// `SessionKeyRegister`, or established at login time). Returning the hint
594/// as the acting pubkey lets the per-tree gate check the actual key the
595/// caller wants to act as, not the connection-wide login key.
596///
597/// An absent hint defaults to the login pubkey — matches the pre-keyset
598/// behavior where every op acted as the login identity.
599fn resolve_acting_pubkey(
600    identity: &SigKey,
601    login_pubkey: &PublicKey,
602    session_keyset: &HashSet<PublicKey>,
603) -> crate::Result<PublicKey> {
604    match &identity.hint().pubkey {
605        Some(claimed) if session_keyset.contains(claimed) => Ok(claimed.clone()),
606        Some(claimed) => Err(crate::Error::Auth(Box::new(
607            AuthError::SigningKeyMismatch {
608                reason: format!(
609                    "request identity claims pubkey '{claimed}' but it is not in the session keyset; \
610                     register it first via SessionKeyChallenge/SessionKeyRegister"
611                ),
612            },
613        ))),
614        None => Ok(login_pubkey.clone()),
615    }
616}
617
618/// Dispatch a Database-level op against the server's local `Database`.
619///
620/// Additive sibling of `dispatch_backend_op`. The caller has already verified
621/// the session identity and run the unconditional per-tree permission gate on
622/// `root_id`. Because the server runs the `Database` layer here, verify-on-read
623/// and the Verified frontier are server-side **by construction**.
624async fn dispatch_database_op(
625    instance: &Instance,
626    ctx: &ConnectionContext,
627    acting_pubkey: &PublicKey,
628    identity: &SigKey,
629    user_uuid: &str,
630    root_id: ID,
631    op: DatabaseOp,
632) -> crate::Result<ServiceResponse> {
633    match op {
634        DatabaseOp::GetEntry { id } => {
635            let entry = instance.backend().get(&id).await?;
636            // Post-fetch owning-tree Read gate: a raw entry id carries no
637            // inline tree, so the pre-dispatch gate (which keys on `root_id`)
638            // could not cover the entry's real owning tree.
639            gate_entry_read(instance, acting_pubkey, identity, &entry).await?;
640            Ok(ServiceResponse::Entry(entry))
641        }
642
643        DatabaseOp::GetVerifiedTips => {
644            // The server runs the Database layer, so `snapshot()` returns the
645            // Verified frontier by construction — no client-side verify, no
646            // remote-detection heuristic.
647            let db = Database::open(instance, &root_id).await?;
648            let snapshot = db.snapshot().await?;
649            Ok(ServiceResponse::Ids(snapshot))
650        }
651
652        DatabaseOp::SubmitSignedEntry { entry } => {
653            // The client signed this entry; the server does NOT trust its
654            // claimed validity. Store it `Unverified`, then run our OWN
655            // verification pass against the entry's pinned settings. A
656            // poisoned entry never reaches `Verified` and is excluded from
657            // every default read by the frontier cut — D1 is closed by
658            // construction here, not by gate-hardening a raw `Put`.
659            //
660            // Only settled-state writes trigger an event: the
661            // `put_entry(.., Unverified, ..)` is a no-fire path by
662            // design (see `Instance::put_entry`), and `Database::verify`
663            // fires its own batched `Verified` event for any entries
664            // the pass settles. The handler just chains the two — no
665            // extra fire bookkeeping here. Note this gates the trigger
666            // only; the event's raw-frontier cursors can still bracket
667            // an unsettled tip (see `Notification` rustdoc).
668            instance
669                .put_entry(
670                    &root_id,
671                    VerificationStatus::Unverified,
672                    *entry,
673                    WriteSource::Remote,
674                )
675                .await?;
676            Database::open(instance, &root_id).await?.verify().await?;
677            Ok(ServiceResponse::Ok)
678        }
679
680        DatabaseOp::BeginTransaction { stores, scope } => {
681            // Single-sourced: both this handler and the Phase-3 remote seam
682            // call `Database::transaction_context`, so `Transaction::commit`'s
683            // build-sign path has one source of truth.
684            let db = Database::open(instance, &root_id).await?;
685            let ctx = db.transaction_context(&stores, scope).await?;
686            Ok(ServiceResponse::TransactionContext(ctx))
687        }
688
689        DatabaseOp::GetStoreState { store } => {
690            // Server-materialized merged state (unencrypted stores only).
691            // Encrypted stores must use GetStoreEntries instead — the
692            // ephemeral transaction here has no encryptor, and Doc
693            // deserialization would fail on ciphertext.
694            let db = Database::open(instance, &root_id).await?;
695            let value = db.get_store_state(&store).await?;
696            Ok(ServiceResponse::CrdtValue(value))
697        }
698
699        DatabaseOp::GetStoreEntries { store, tips, scope } => {
700            // Universal primitive (encrypted + unencrypted): returns raw
701            // Entry records with opaque data in canonical CRDT replay order.
702            // For encrypted stores the client decrypts+merges locally.
703            let db = Database::open(instance, &root_id).await?;
704            let entries = db.get_store_entries(&store, &tips, scope).await?;
705            Ok(ServiceResponse::Entries(entries))
706        }
707
708        DatabaseOp::GetStoreTipsUpToEntries { store, up_to } => {
709            let db = Database::open(instance, &root_id).await?;
710            let boundary = crate::Snapshot::from(up_to);
711            let snapshot = db
712                .ops()
713                .store_snapshot_at(&root_id, &store, &boundary)
714                .await?;
715            Ok(ServiceResponse::Ids(snapshot))
716        }
717
718        DatabaseOp::ComputeMergeState { store, entry_ids } => {
719            let db = Database::open(instance, &root_id).await?;
720            let merge_base = db
721                .ops()
722                .find_merge_base(&root_id, &store, &entry_ids)
723                .await?;
724            let path = db
725                .ops()
726                .get_path_from_to(&root_id, &store, &merge_base, &entry_ids)
727                .await?;
728            Ok(ServiceResponse::MergeState(MergeState { merge_base, path }))
729        }
730
731        DatabaseOp::GetCachedCrdtState { store, key } => {
732            // Per-tree Read gate already ran above. Try the caller's own
733            // User-scoped slot first (where client-uploaded ciphertext for
734            // encrypted stores lives), then fall back to Shared (where the
735            // daemon's own materialization of unencrypted stores lives).
736            // The fallback is what gives cross-user dedup on plaintext
737            // stores: alice triggers a server materialization, blob lands
738            // in Shared, bob's later read finds it without recomputing.
739            let backend = instance.require_local_engine()?;
740            let mut blob = backend
741                .get_cached_crdt_state(&CacheScope::User(user_uuid.to_string()), &key, &store)
742                .await?;
743            if blob.is_none() {
744                blob = backend
745                    .get_cached_crdt_state(&CacheScope::Shared, &key, &store)
746                    .await?;
747            }
748            Ok(ServiceResponse::CachedCrdtState(blob))
749        }
750
751        DatabaseOp::CacheCrdtState { store, key, blob } => {
752            // Per-tree Read gate already ran above. Per-user trust: the
753            // blob is opaque (cipher- or plaintext) and stored verbatim;
754            // only the submitting user can read it back. We never promote
755            // a client upload to Shared — the daemon can't verify the
756            // merge result, so cross-user visibility would be a poison
757            // vector. Shared writes only come from the daemon's own
758            // in-process (LocalBackend) materialization path.
759            instance
760                .require_local_engine()?
761                .cache_crdt_state(CacheScope::User(user_uuid.to_string()), &key, &store, blob)
762                .await?;
763            Ok(ServiceResponse::Ok)
764        }
765
766        DatabaseOp::SetInstanceMetadata { metadata } => {
767            // Admin-on-`_databases` gate already ran in the dispatcher (against
768            // the server-known system tree, not `root_id`).
769            instance.backend().set_instance_metadata(&metadata).await?;
770            Ok(ServiceResponse::Ok)
771        }
772
773        DatabaseOp::SubscribeWrites { tips } => {
774            // Per-tree Read gate already ran in the dispatcher.
775            //
776            // Subscription is just a per-db callback registered against the
777            // daemon's Instance. The callback's body pushes a notification
778            // frame into this connection's writer channel; the daemon's
779            // existing `fire_write_callbacks` dispatch handles fan-out by
780            // walking the per-tree callback list. No separate registry or
781            // global-publisher hook needed.
782            //
783            // Idempotent: a tree this connection has already subscribed to
784            // is a no-op (we'd otherwise register a second callback that
785            // pushes a duplicate frame on every write). Take the lock
786            // briefly to early-out, then release before any await — the
787            // std::Mutex isn't `Send` and can't cross an await point.
788            //
789            // Under the current dispatch shape the per-connection request
790            // loop is single-threaded (one frame → one dispatch → one
791            // response), so a second `SubscribeWrites` for the same tree
792            // on the same connection can only arrive *after* the first
793            // call has fully completed and recorded its subscription. The
794            // post-await Entry-API guard below is therefore **defensive
795            // against a future shape change** (per-connection parallel
796            // dispatch, or another path that takes `ctx` and registers
797            // callbacks concurrently) rather than fixing a present race.
798            // Cheap to keep — one Entry-API call plus a single
799            // `remove_write_callback` in the unreachable Occupied arm.
800            {
801                let subs = ctx.subscribed_lock();
802                if subs.contains_key(&root_id) {
803                    return Ok(ServiceResponse::Ok);
804                }
805            }
806            let tx = ctx.tx.clone();
807            // Capture the initial cursor and register the callback atomically
808            // under the tree lock. The fire path (`put_entry` /
809            // `Database::verify`) holds this same lock while advancing cursors,
810            // so no write can land between the tips snapshot and the
811            // registration: the empty-`tips` cursor is exactly the daemon's
812            // frontier at the instant this subscription begins, with no
813            // lost-update gap. Safe to hold across the `snapshot` await —
814            // `Instance::snapshot` reads the raw backend (no tree lock) and,
815            // unlike the fire path, we spawn/await no user callback under the
816            // guard, so there is no reentrancy.
817            let tree_lock = instance.tree_lock(&root_id);
818            let tree_guard = tree_lock.lock().await;
819            // Initial cursor: the client's supplied `tips` if non-empty;
820            // otherwise the daemon's current tips at subscribe-time (the "I have
821            // no initial state; give me events from now" posture documented on
822            // the wire variant). The cursor advances per fire as usual.
823            let initial_tips = if tips.is_empty() {
824                // Propagate rather than defaulting: an error here would seed
825                // the daemon-side cursor at `EMPTY`, which is the "replay
826                // from the beginning" value rather than "start from now".
827                instance.snapshot(&root_id).await?
828            } else {
829                tips
830            };
831            let id = instance.register_write_callback(
832                root_id.clone(),
833                initial_tips,
834                move |event, db| {
835                    // ORDERING INVARIANT — DO NOT add an `.await` before the
836                    // `tx.send` below. Per-tree notifications are delivered in
837                    // daemon-canonical order, and that guarantee rests entirely
838                    // on this send running *synchronously*, in the closure's
839                    // pre-future prefix. `Instance::spawn_write_callbacks`
840                    // invokes this closure while the caller holds the tree's
841                    // `tree_lock` (see `put_entry` / `Database::verify`), and it
842                    // advances each subscription's cursor in the same critical
843                    // section. Because the send happens here — before any await,
844                    // still under that lock, in cursor-advance order — two
845                    // connections writing the same tree concurrently cannot
846                    // interleave their frames: writer A's send completes and A
847                    // drops the lock before writer B can acquire it and send.
848                    // The returned future is deliberately a no-op; only that
849                    // (empty) tail is spawned and drained lock-free. Moving the
850                    // send *into* the future — or awaiting anything before it —
851                    // would push it out of the locked section onto the runtime's
852                    // scheduler and let same-tree frames reorder (the bug fixed
853                    // in "preserve write-notification order under concurrent
854                    // writers"). `mpsc::UnboundedSender::send` is non-blocking
855                    // and takes `&self`, which is exactly what makes a
856                    // synchronous send here possible.
857                    //
858                    // Send failure (writer task gone) is silently dropped; the
859                    // connection is about to be torn down and the guard's Drop
860                    // will unregister us next.
861                    //
862                    // The closure only fires for settled-state writes today
863                    // (Verified). Unverified writes go through `put_entry`
864                    // without firing `fire_write_callbacks`, so no notification
865                    // is ever *triggered* by them — though the cursors shipped
866                    // here are raw frontiers and can bracket an unsettled tip.
867                    let frame = ServerFrame::Notification(Notification::DatabaseWrite {
868                        root_id: db.root_id().clone(),
869                        previous_tips: event.previous_tips().clone(),
870                        post_tips: event.post_tips().clone(),
871                        source: event.source(),
872                    });
873                    let _ = tx.send(frame);
874                    async move { Ok(()) }
875                },
876            );
877            // Cursor captured and callback live; release the tree lock before
878            // touching the std subscribed-lock below (never nest the two).
879            drop(tree_guard);
880            // Re-acquire the lock to record this connection's subscription.
881            // Defensive Entry-API guard: in the current shape (single-
882            // threaded per-connection dispatch) the Occupied arm is
883            // unreachable — see the comment above the early-out check.
884            // Kept so that a future shape change (parallel dispatch per
885            // connection, or another concurrent path that takes `ctx`)
886            // can't leave us with two callbacks pushing duplicate frames
887            // on every write; the loser drops its just-registered
888            // callback and the first registration stays.
889            use std::collections::hash_map::Entry as MapEntry;
890            let mut subs = ctx.subscribed_lock();
891            match subs.entry(root_id.clone()) {
892                MapEntry::Vacant(slot) => {
893                    slot.insert(id);
894                }
895                MapEntry::Occupied(_) => {
896                    drop(subs);
897                    instance.remove_write_callback(&root_id, id);
898                }
899            }
900            Ok(ServiceResponse::Ok)
901        }
902
903        DatabaseOp::UnsubscribeWrites => {
904            // Per-tree Read gate already ran. Idempotent: unsubscribing a
905            // tree this connection was not subscribed to is a no-op.
906            let removed = ctx.subscribed_lock().remove(&root_id);
907            if let Some(id) = removed {
908                instance.remove_write_callback(&root_id, id);
909            }
910            Ok(ServiceResponse::Ok)
911        }
912    }
913}
914
915/// Handle `ServiceRequest::TrustedLoginUser`: look up the user's full record,
916/// mint a challenge, and move the connection into `AwaitingProof`.
917async fn handle_trusted_login_user(
918    instance: &Instance,
919    state: &mut ConnectionState,
920    username: String,
921) -> crate::Result<ServiceResponse> {
922    // Look up the full `UserInfo`. The encrypted root key + salt ship to the
923    // client so it can decrypt locally and sign the challenge in the same
924    // round-trip; the daemon never sees the password or the plaintext key.
925    // The non-credential fields (user_database_id, status) ride along so the
926    // client can build the `User` session after proof without a second wire
927    // read of `_users`. If the lookup fails (no such user, disabled,
928    // duplicates), drop back to `PreAuth` and bubble the error.
929    let users_db = instance.users_db().await?;
930    let (user_uuid, user_info) = match lookup_user_record(&users_db, &username).await {
931        Ok(v) => v,
932        Err(e) => {
933            *state = ConnectionState::PreAuth;
934            return Err(e);
935        }
936    };
937
938    let expected_pubkey = user_info.credentials.root_key_id.clone();
939    let challenge = generate_challenge();
940    *state = ConnectionState::AwaitingProof {
941        username,
942        user_uuid: user_uuid.clone(),
943        challenge: challenge.clone(),
944        expected_pubkey,
945    };
946    Ok(ServiceResponse::TrustedLoginChallenge {
947        challenge,
948        user_uuid,
949        user_info,
950    })
951}
952
953/// Handle `ServiceRequest::TrustedLoginProve`: verify the signature against the
954/// stored challenge and either transition to `Authenticated` or drop back
955/// to `PreAuth`.
956fn handle_trusted_login_prove(
957    state: &mut ConnectionState,
958    signature: &[u8],
959) -> crate::Result<ServiceResponse> {
960    let (username, user_uuid, challenge, expected_pubkey) =
961        match std::mem::replace(state, ConnectionState::PreAuth) {
962            ConnectionState::AwaitingProof {
963                username,
964                user_uuid,
965                challenge,
966                expected_pubkey,
967            } => (username, user_uuid, challenge, expected_pubkey),
968            other => {
969                // Restore the (unexpected) state we just took out so subsequent
970                // requests see consistent state.
971                *state = other;
972                return Err(crate::Error::Io(std::io::Error::new(
973                    std::io::ErrorKind::InvalidData,
974                    "TrustedLoginProve received outside of AwaitingProof state",
975                )));
976            }
977        };
978
979    match verify_challenge_response(&challenge, signature, &expected_pubkey) {
980        Ok(()) => {
981            let mut session_keyset = HashSet::new();
982            session_keyset.insert(expected_pubkey.clone());
983            *state = ConnectionState::Authenticated {
984                username,
985                user_uuid,
986                login_pubkey: expected_pubkey,
987                session_keyset,
988                pending_key_challenges: HashMap::new(),
989            };
990            Ok(ServiceResponse::TrustedLoginOk)
991        }
992        Err(e) => {
993            // Already reset to PreAuth via the mem::replace above.
994            Err(crate::Error::Auth(Box::new(e)))
995        }
996    }
997}
998
999/// Handle `ServiceRequest::SessionKeyChallenge`: mint a single-use challenge
1000/// bound to `pubkey` and stash it in the connection's pending-challenges map.
1001///
1002/// Requires an authenticated connection. A repeat call for the same pubkey
1003/// overwrites the prior challenge — last-issued wins, so a stale challenge
1004/// can't be replayed.
1005fn handle_session_key_challenge(
1006    state: &mut ConnectionState,
1007    pubkey: PublicKey,
1008) -> crate::Result<ServiceResponse> {
1009    match state {
1010        ConnectionState::Authenticated {
1011            pending_key_challenges,
1012            ..
1013        } => {
1014            let challenge = generate_challenge();
1015            pending_key_challenges.insert(pubkey, challenge.clone());
1016            Ok(ServiceResponse::SessionKeyChallenge { challenge })
1017        }
1018        _ => Err(crate::Error::Auth(Box::new(
1019            AuthError::InvalidAuthConfiguration {
1020                reason: "SessionKeyChallenge requires an authenticated connection; \
1021                     complete TrustedLogin* first"
1022                    .to_string(),
1023            },
1024        ))),
1025    }
1026}
1027
1028/// Handle `ServiceRequest::SessionKeyRegister`: verify the signature against
1029/// the matching pending challenge and, on success, add `pubkey` to the
1030/// connection's session keyset.
1031///
1032/// The challenge is consumed (removed) whether verification succeeds or fails,
1033/// so a bad signature can't be retried against the same challenge.
1034fn handle_session_key_register(
1035    state: &mut ConnectionState,
1036    pubkey: PublicKey,
1037    signature: &[u8],
1038) -> crate::Result<ServiceResponse> {
1039    match state {
1040        ConnectionState::Authenticated {
1041            session_keyset,
1042            pending_key_challenges,
1043            ..
1044        } => {
1045            let challenge = pending_key_challenges.remove(&pubkey).ok_or_else(|| {
1046                crate::Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
1047                    reason: format!(
1048                        "no outstanding SessionKeyChallenge for pubkey '{pubkey}'; \
1049                         issue the challenge before registering"
1050                    ),
1051                }))
1052            })?;
1053            verify_challenge_response(&challenge, signature, &pubkey)
1054                .map_err(|e| crate::Error::Auth(Box::new(e)))?;
1055            session_keyset.insert(pubkey);
1056            Ok(ServiceResponse::Ok)
1057        }
1058        _ => Err(crate::Error::Auth(Box::new(
1059            AuthError::InvalidAuthConfiguration {
1060                reason: "SessionKeyRegister requires an authenticated connection; \
1061                     complete TrustedLogin* first"
1062                    .to_string(),
1063            },
1064        ))),
1065    }
1066}
1067
1068/// Resolve `pubkey`'s permission against `tree_id`'s `auth_settings` and reject
1069/// the request if the resolved level doesn't cover `required`.
1070///
1071/// If the database doesn't exist on this daemon yet and `require_existing`
1072/// is false, the gate passes through so the dispatched op surfaces its own
1073/// response (NotFound, empty result, or — for write coordination — a
1074/// no-op). This is what keeps the legitimate "create a new database" flow
1075/// working: `Database::create` reads tips on the tree before its root entry
1076/// has propagated, so an outright denial here would break creation. The
1077/// cost is that callers can still distinguish "no such database" from
1078/// "exists but no access"; closing that existence-leak channel is filed as
1079/// a follow-up.
1080///
1081/// `require_existing = true` flips that to **fail closed**: an absent
1082/// target is denied rather than waved through. Used for the
1083/// `SetInstanceMetadata` admin gate (D8) — `_databases` always exists on an
1084/// initialized daemon and that op is never a creation flow, so the
1085/// create-flow passthrough there would only ever be a fail-open hole that
1086/// lets any authenticated user rewrite the daemon's system-DB pointers if
1087/// `_databases` were ever unreadable.
1088///
1089/// When the gate does fire, the denial error is the same shape regardless
1090/// of which sub-check failed (key not in auth_settings, mismatched hint,
1091/// insufficient permission level): no internal detail about *why* leaks
1092/// back over the wire.
1093///
1094/// System databases (`_users`, `_databases`, `_sync`, `_instance`) are gated
1095/// like any other tree: callers must hold the required permission in the
1096/// system DB's `auth_settings`. The instance-admin bootstrap (first user on
1097/// the device) writes the first user as `Admin(0)` on `_users` and
1098/// `_databases`, which is how legitimate administrative access is granted —
1099/// the previous hardcoded read exemption is gone. The daemon's device-keyed
1100/// local path still handles internal system-database maintenance writes that
1101/// originate inside the server.
1102async fn gate_tree_permission(
1103    instance: &Instance,
1104    pubkey: &PublicKey,
1105    identity: &SigKey,
1106    tree_id: &ID,
1107    required: Permission,
1108    require_existing: bool,
1109) -> crate::Result<()> {
1110    let denied = || {
1111        crate::Error::Auth(Box::new(AuthError::PermissionDenied {
1112            reason: format!("tree {tree_id}: pubkey {pubkey} not permitted for {required:?}"),
1113        }))
1114    };
1115
1116    if !instance.has_database(tree_id).await {
1117        return if require_existing {
1118            Err(denied())
1119        } else {
1120            Ok(())
1121        };
1122    }
1123
1124    let database = Database::open(instance, tree_id).await?;
1125    let settings_store = database.get_settings().await?;
1126    let auth_settings = settings_store.auth_snapshot().await?;
1127
1128    let resolved =
1129        match resolve_identity_permission(pubkey, identity, &auth_settings, Some(instance)).await {
1130            Ok(p) => p,
1131            // Resolution failures (key not found, mismatch, etc.) collapse to the
1132            // same shape as an insufficient-permission denial, so the client
1133            // can't tell whether its identity was unknown or merely too low.
1134            Err(_) => return Err(denied()),
1135        };
1136
1137    let allowed = match required {
1138        Permission::Read => true,
1139        Permission::Write(_) => resolved.can_write(),
1140        Permission::Admin(_) => resolved.can_admin(),
1141    };
1142
1143    if !allowed {
1144        return Err(denied());
1145    }
1146
1147    Ok(())
1148}
1149
1150/// Per-tree read gate for ops keyed by a raw entry id, which therefore
1151/// carry no inline tree id and never hit the pre-dispatch `tree_id()` gate
1152/// (`Get`). The tree to authorise against is only knowable *after* the
1153/// fetch: it is the entry's claimed `tree.root`, or — for a tree-root
1154/// entry, whose `root()` is `None` — the entry's own id.
1155///
1156/// Model B (hard multi-tenant boundary): system DBs are unencrypted and
1157/// protected solely by this gate, so a raw cross-tree `Get` MUST resolve
1158/// and check the real owning tree before returning content. Delegates to
1159/// `gate_tree_permission`, so the `has_database`-absent passthrough and the
1160/// opaque denial shape are identical to the inline-tree-id path.
1161async fn gate_entry_read(
1162    instance: &Instance,
1163    pubkey: &PublicKey,
1164    identity: &SigKey,
1165    entry: &crate::entry::Entry,
1166) -> crate::Result<()> {
1167    let owning_tree = entry.root().unwrap_or_else(|| entry.id());
1168    // create-flow passthrough (false): a Get against a tree not yet
1169    // registered on this daemon must be waved through, same as the
1170    // inline-tree-id path.
1171    gate_tree_permission(
1172        instance,
1173        pubkey,
1174        identity,
1175        &owning_tree,
1176        Permission::Read,
1177        false,
1178    )
1179    .await
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184    use super::*;
1185    use crate::backend::database::InMemory;
1186    use crate::service::protocol::{Handshake, write_frame};
1187
1188    /// Helper: start a server on a temp socket, return path + shutdown sender.
1189    async fn start_test_server() -> (PathBuf, watch::Sender<()>, Instance) {
1190        let dir = tempfile::tempdir().unwrap();
1191        let socket_path = dir.keep().join("test.sock");
1192        let (instance, _admin) = Instance::create_backend(
1193            Box::new(InMemory::new()),
1194            crate::NewUser::passwordless("admin"),
1195        )
1196        .await
1197        .unwrap();
1198        let (tx, rx) = watch::channel(());
1199        let server = ServiceServer::new(instance.clone(), socket_path.clone());
1200        tokio::spawn(async move {
1201            let _ = server.run(rx).await;
1202        });
1203        // Wait for the socket to appear (server binds asynchronously). Poll
1204        // with a short sleep instead of a fixed delay so a slow sandbox
1205        // (where this test was occasionally flaky under `nix build`) doesn't
1206        // race the bind step.
1207        for _ in 0..50 {
1208            if socket_path.exists() {
1209                break;
1210            }
1211            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1212        }
1213        (socket_path, tx, instance)
1214    }
1215
1216    #[tokio::test]
1217    async fn test_server_starts_and_shuts_down() {
1218        let (socket_path, tx, _instance) = start_test_server().await;
1219        assert!(socket_path.exists());
1220        drop(tx);
1221        // Poll for cleanup with the same robustness as the bind wait.
1222        for _ in 0..50 {
1223            if !socket_path.exists() {
1224                break;
1225            }
1226            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1227        }
1228        // Socket should be cleaned up
1229        assert!(!socket_path.exists());
1230    }
1231
1232    #[tokio::test]
1233    async fn test_wrong_protocol_version() {
1234        let (socket_path, _tx, _instance) = start_test_server().await;
1235
1236        let stream = tokio::net::UnixStream::connect(&socket_path).await.unwrap();
1237        let (mut reader, mut writer) = tokio::io::split(stream);
1238
1239        // Send wrong version
1240        let handshake = Handshake {
1241            protocol_version: 999,
1242        };
1243        write_frame(&mut writer, &handshake).await.unwrap();
1244
1245        // Read ack (server sends its version back)
1246        let ack: Option<HandshakeAck> = read_frame(&mut reader).await.unwrap();
1247        let ack = ack.unwrap();
1248        assert_eq!(ack.protocol_version, PROTOCOL_VERSION);
1249
1250        // Connection should be closed by server after version mismatch
1251        // Next read should get EOF
1252        let result: crate::Result<Option<ServiceResponse>> = read_frame(&mut reader).await;
1253        assert!(result.unwrap().is_none());
1254    }
1255
1256    /// Load-bearing invariant: `ConnectionState` must never hold plaintext
1257    /// signing material in any variant. The daemon participates in storage
1258    /// and challenge-response only; the rejected Branch A design held
1259    /// decrypted user keys server-side and that boundary was reinstated by
1260    /// design (see Service Architecture doc § Decision record).
1261    ///
1262    /// Structure of the test: construct each variant, destructure with named
1263    /// fields (so adding a field forces this test to be edited), and check
1264    /// the static type of each field is not `PrivateKey`. A future refactor
1265    /// that adds e.g. `decrypted_root_key: PrivateKey` to `Authenticated`
1266    /// would either fail the type check at runtime or fail the destructure
1267    /// match exhaustiveness at compile time.
1268    #[test]
1269    fn connection_state_never_holds_private_key() {
1270        use crate::auth::crypto::{PrivateKey, generate_keypair};
1271        use std::any::TypeId;
1272
1273        fn assert_not_private_key<T: 'static>(_value: &T, label: &str) {
1274            assert_ne!(
1275                TypeId::of::<T>(),
1276                TypeId::of::<PrivateKey>(),
1277                "ConnectionState field `{label}` is PrivateKey — daemon must not hold plaintext keys"
1278            );
1279        }
1280
1281        let (_signing, pubkey) = generate_keypair();
1282        let states = [
1283            ConnectionState::PreAuth,
1284            ConnectionState::AwaitingProof {
1285                username: "u".to_string(),
1286                user_uuid: "uu".to_string(),
1287                challenge: vec![1, 2, 3],
1288                expected_pubkey: pubkey.clone(),
1289            },
1290            ConnectionState::Authenticated {
1291                username: "u".to_string(),
1292                user_uuid: "uu".to_string(),
1293                login_pubkey: pubkey.clone(),
1294                session_keyset: {
1295                    let mut s = HashSet::new();
1296                    s.insert(pubkey);
1297                    s
1298                },
1299                pending_key_challenges: HashMap::new(),
1300            },
1301        ];
1302
1303        for state in &states {
1304            match state {
1305                ConnectionState::PreAuth => {}
1306                ConnectionState::AwaitingProof {
1307                    username,
1308                    user_uuid,
1309                    challenge,
1310                    expected_pubkey,
1311                } => {
1312                    assert_not_private_key(username, "AwaitingProof::username");
1313                    assert_not_private_key(user_uuid, "AwaitingProof::user_uuid");
1314                    assert_not_private_key(challenge, "AwaitingProof::challenge");
1315                    assert_not_private_key(expected_pubkey, "AwaitingProof::expected_pubkey");
1316                }
1317                ConnectionState::Authenticated {
1318                    username,
1319                    user_uuid,
1320                    login_pubkey,
1321                    session_keyset,
1322                    pending_key_challenges,
1323                } => {
1324                    assert_not_private_key(username, "Authenticated::username");
1325                    assert_not_private_key(user_uuid, "Authenticated::user_uuid");
1326                    assert_not_private_key(login_pubkey, "Authenticated::login_pubkey");
1327                    for k in session_keyset {
1328                        assert_not_private_key(k, "Authenticated::session_keyset entry");
1329                    }
1330                    for (k, ch) in pending_key_challenges {
1331                        assert_not_private_key(k, "Authenticated::pending_key_challenges key");
1332                        assert_not_private_key(
1333                            ch,
1334                            "Authenticated::pending_key_challenges challenge",
1335                        );
1336                    }
1337                }
1338            }
1339        }
1340    }
1341
1342    #[tokio::test]
1343    async fn test_authenticated_request_rejected_without_login() {
1344        // Companion to the integration test `test_unauthenticated_backend_op_rejected`
1345        // — exercises the same gate path against the raw protocol so a
1346        // regression here surfaces immediately, not just at the
1347        // `RemoteConnection` layer.
1348        let (socket_path, _tx, _instance) = start_test_server().await;
1349
1350        let stream = tokio::net::UnixStream::connect(&socket_path).await.unwrap();
1351        let (mut reader, mut writer) = tokio::io::split(stream);
1352
1353        write_frame(
1354            &mut writer,
1355            &Handshake {
1356                protocol_version: PROTOCOL_VERSION,
1357            },
1358        )
1359        .await
1360        .unwrap();
1361        let _ack: Option<HandshakeAck> = read_frame(&mut reader).await.unwrap();
1362
1363        // Send an AuthenticatedDb request without completing TrustedLogin.
1364        write_frame(
1365            &mut writer,
1366            &ServiceRequest::AuthenticatedDb(Box::new(AuthenticatedDbRequest {
1367                root_id: crate::entry::ID::default(),
1368                identity: crate::auth::types::SigKey::default(),
1369                op: DatabaseOp::GetEntry {
1370                    id: crate::entry::ID::from_bytes("nonexistent"),
1371                },
1372            })),
1373        )
1374        .await
1375        .unwrap();
1376
1377        let frame: Option<ServerFrame> = read_frame(&mut reader).await.unwrap();
1378        let resp = match frame.unwrap() {
1379            ServerFrame::Response(r) => *r,
1380            other => panic!("Expected Response frame, got {other:?}"),
1381        };
1382        match resp {
1383            ServiceResponse::Error(e) => {
1384                assert_eq!(
1385                    e.module, "auth",
1386                    "expected an auth-module error from the gate; got {e:?}"
1387                );
1388            }
1389            other => panic!("Expected gate Error, got {other:?}"),
1390        }
1391    }
1392
1393    #[tokio::test]
1394    async fn test_get_instance_metadata() {
1395        let (socket_path, _tx, _instance) = start_test_server().await;
1396
1397        let stream = tokio::net::UnixStream::connect(&socket_path).await.unwrap();
1398        let (mut reader, mut writer) = tokio::io::split(stream);
1399
1400        // Handshake
1401        write_frame(
1402            &mut writer,
1403            &Handshake {
1404                protocol_version: PROTOCOL_VERSION,
1405            },
1406        )
1407        .await
1408        .unwrap();
1409        let _ack: Option<HandshakeAck> = read_frame(&mut reader).await.unwrap();
1410
1411        // Request metadata
1412        write_frame(&mut writer, &ServiceRequest::GetInstanceMetadata)
1413            .await
1414            .unwrap();
1415
1416        let frame: Option<ServerFrame> = read_frame(&mut reader).await.unwrap();
1417        let resp = match frame.unwrap() {
1418            ServerFrame::Response(r) => *r,
1419            other => panic!("Expected Response frame, got {other:?}"),
1420        };
1421        match resp {
1422            ServiceResponse::InstanceMetadata(Some(_meta)) => {
1423                // Server was initialized so metadata should exist
1424            }
1425            other => panic!("Expected InstanceMetadata(Some), got {other:?}"),
1426        }
1427    }
1428
1429    #[tokio::test]
1430    async fn test_stale_socket_cleanup() {
1431        let dir = tempfile::tempdir().unwrap();
1432        let socket_path = dir.path().join("test.sock");
1433
1434        // Create a stale socket file
1435        tokio::fs::write(&socket_path, "stale").await.unwrap();
1436        assert!(socket_path.exists());
1437
1438        let (instance, _admin) = Instance::create_backend(
1439            Box::new(InMemory::new()),
1440            crate::NewUser::passwordless("admin"),
1441        )
1442        .await
1443        .unwrap();
1444        let (_tx, rx) = watch::channel(());
1445        let server = ServiceServer::new(instance, socket_path.clone());
1446
1447        // Server should remove stale socket and bind successfully
1448        let handle = tokio::spawn(async move { server.run(rx).await });
1449
1450        // The server binds asynchronously; poll until it accepts a connection
1451        // rather than racing a fixed sleep (flaky under parallel test load).
1452        let mut stream = None;
1453        for _ in 0..200 {
1454            if let Ok(s) = tokio::net::UnixStream::connect(&socket_path).await {
1455                stream = Some(s);
1456                break;
1457            }
1458            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1459        }
1460        assert!(
1461            stream.is_some(),
1462            "server did not bind a connectable socket in time"
1463        );
1464
1465        handle.abort();
1466    }
1467
1468    /// D8 regression: `require_existing` flips the create-flow passthrough
1469    /// to fail-closed. An absent target is waved through with `false` (the
1470    /// `Database::create` path) but denied with `true` (the
1471    /// `SetInstanceMetadata` admin gate), so an unreadable `_databases`
1472    /// can't become a fail-open hole.
1473    #[tokio::test]
1474    async fn test_gate_require_existing_fails_closed_on_absent_db() {
1475        use crate::auth::crypto::generate_keypair;
1476
1477        let (instance, _admin) = Instance::create_backend(
1478            Box::new(InMemory::new()),
1479            crate::NewUser::passwordless("admin"),
1480        )
1481        .await
1482        .unwrap();
1483        let (_sk, pubkey) = generate_keypair();
1484        let absent = ID::from_bytes("no-such-tree");
1485
1486        gate_tree_permission(
1487            &instance,
1488            &pubkey,
1489            &SigKey::default(),
1490            &absent,
1491            Permission::Admin(0),
1492            false,
1493        )
1494        .await
1495        .expect("create-flow passthrough must wave an absent tree through");
1496
1497        let err = gate_tree_permission(
1498            &instance,
1499            &pubkey,
1500            &SigKey::default(),
1501            &absent,
1502            Permission::Admin(0),
1503            true,
1504        )
1505        .await
1506        .expect_err("require_existing must deny an absent tree");
1507        assert!(
1508            matches!(&err, crate::Error::Auth(b) if matches!(**b, AuthError::PermissionDenied { .. })),
1509            "expected PermissionDenied, got: {err:?}",
1510        );
1511    }
1512}