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::fs::{File, OpenOptions, TryLockError};
8use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt, PermissionsExt};
9use std::path::{Component, Path, PathBuf};
10use std::sync::Arc;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::time::{Duration, Instant};
13
14use tokio::net::UnixListener;
15use tokio::sync::{mpsc, watch};
16use tokio::task::JoinSet;
17
18use crate::Instance;
19use crate::auth::crypto::{PublicKey, generate_challenge, verify_challenge_response};
20use crate::auth::errors::AuthError;
21use crate::auth::types::{Permission, SigKey};
22use crate::auth::validation::permissions::resolve_identity_permission;
23use crate::backend::{
24    CacheScope, StagingToken, StoreStateLifecycle, StoreStateRequest, VerificationStatus,
25};
26use crate::database::Database;
27use crate::entry::ID;
28use crate::instance::{CallbackId, WriteSource};
29use crate::service::error::ServiceError;
30use crate::service::protocol::{
31    AuthenticatedDbRequest, DatabaseOp, HandshakeAck, MergeState, Notification, PROTOCOL_VERSION,
32    ServerFrame, ServiceRequest, ServiceResponse, read_frame, write_frame,
33};
34use crate::user::system_databases::lookup_user_record;
35
36/// Connection identifier. Monotonic per server-run; reused only after
37/// `AtomicU64` wraps (effectively never on a real daemon). Purely
38/// diagnostic now — registry-based routing went away in the per-db
39/// callback refactor.
40type ConnectionId = u64;
41
42/// How long an idle session token survives before the next dispatch reclaims
43/// its backend resources.
44const TOKEN_IDLE_TTL: Duration = Duration::from_secs(5 * 60);
45
46/// One server-issued Store-state capability, bound to the session that minted
47/// it. `published` distinguishes a readable view from a staging namespace that
48/// still owes an abort if the session goes away.
49struct SessionStaging {
50    backend: StagingToken,
51    user_uuid: String,
52    database: ID,
53    published: bool,
54    chunks: HashMap<u64, Vec<u8>>,
55    last_used: Instant,
56}
57
58/// Per-connection context carried through the dispatch chain. Holds:
59///
60/// - `tx`: the writer-channel sender — both `ServerFrame::Response`s from
61///   the dispatcher and `ServerFrame::Notification`s from subscribed
62///   per-db callbacks ride through this same channel, so the order
63///   observed by the client is the order frames hit `frame_tx`.
64/// - `instance`: needed in `Drop` to call `remove_write_callback` for
65///   the connection's registered subscriptions.
66/// - `subscribed`: `root_id -> CallbackId` for every `SubscribeWrites`
67///   this connection has done. Cleaned up on disconnect (via the guard's
68///   `Drop`) and on explicit `UnsubscribeWrites`.
69///
70/// There is no per-connection registry on the server. Each subscription
71/// is just a per-db callback registered against the daemon's Instance
72/// (`Instance::register_write_callback`); the daemon's existing
73/// `fire_write_callbacks` dispatch path handles fan-out by walking the
74/// per-tree callback list, no separate fan-out mechanism required.
75struct ConnectionContext {
76    conn_id: ConnectionId,
77    tx: mpsc::UnboundedSender<ServerFrame>,
78    instance: Instance,
79    subscribed: std::sync::Mutex<HashMap<ID, CallbackId>>,
80    staging: std::sync::Mutex<HashMap<String, SessionStaging>>,
81    token_idle_ttl: Duration,
82}
83
84impl ConnectionContext {
85    fn subscribed_lock(&self) -> std::sync::MutexGuard<'_, HashMap<ID, CallbackId>> {
86        self.subscribed
87            .lock()
88            .unwrap_or_else(|poisoned| poisoned.into_inner())
89    }
90
91    /// Drop session tokens that have gone idle, returning the backend staging
92    /// tokens whose namespaces the caller must abort.
93    fn cleanup_expired(&self) -> Vec<StagingToken> {
94        let now = Instant::now();
95        let mut expired = Vec::new();
96        self.staging.lock().unwrap().retain(|_, value| {
97            let keep = now.duration_since(value.last_used) <= self.token_idle_ttl;
98            if !keep && !value.published {
99                expired.push(value.backend.clone());
100            }
101            keep
102        });
103        expired
104    }
105}
106
107/// RAII cleanup: on connection drop (clean EOF, error, or panic), call
108/// `Instance::remove_write_callback` for every subscription this
109/// connection registered. Holds an `Arc<ConnectionContext>` rather than
110/// borrowing so the cleanup path is identical for every exit case
111/// without sprinkling `remove_*` calls through the connection handler.
112struct ConnectionGuard {
113    ctx: Arc<ConnectionContext>,
114}
115
116impl Drop for ConnectionGuard {
117    fn drop(&mut self) {
118        let subs = self.ctx.subscribed_lock();
119        if !subs.is_empty() {
120            tracing::debug!(
121                conn_id = self.ctx.conn_id,
122                "Unregistering {} subscriptions on disconnect",
123                subs.len()
124            );
125        }
126        for (tree_id, id) in subs.iter() {
127            self.ctx.instance.remove_write_callback(tree_id, *id);
128        }
129        drop(subs);
130        let staging = std::mem::take(&mut *self.ctx.staging.lock().unwrap())
131            .into_values()
132            .filter(|value| !value.published)
133            .map(|value| value.backend)
134            .collect::<Vec<_>>();
135        if !staging.is_empty() {
136            let backend = self.ctx.instance.backend().clone();
137            tokio::spawn(async move {
138                for token in staging {
139                    let _ = backend.abort_store_state(token).await;
140                }
141            });
142        }
143    }
144}
145
146/// Per-connection authentication state.
147///
148/// A connection moves `PreAuth → AwaitingProof → Authenticated` on a successful
149/// `TrustedLoginUser` / `TrustedLoginProve` exchange. A failed proof drops the
150/// connection back to `PreAuth` so the client can retry without reconnecting.
151/// Any other request while in `AwaitingProof` also resets the state so a
152/// half-finished login can't be exploited mid-flight.
153///
154/// "Trusted" means every process that can reach the socket is authorised for
155/// the full service API. The socket directory is the trust boundary; see the
156/// service architecture documentation.
157#[derive(Debug, Clone)]
158enum ConnectionState {
159    /// No login attempt yet, or last attempt failed/abandoned.
160    PreAuth,
161    /// `TrustedLoginUser` succeeded; waiting for the client's `TrustedLoginProve`.
162    AwaitingProof {
163        username: String,
164        user_uuid: String,
165        challenge: Vec<u8>,
166        expected_pubkey: PublicKey,
167    },
168    /// Login completed. `login_pubkey` is the verified root pubkey for the
169    /// user, established at `TrustedLoginProve` time. `session_keyset` is the
170    /// set of pubkeys the client has further proven possession of via
171    /// `SessionKeyChallenge`/`SessionKeyRegister`; it always contains
172    /// `login_pubkey` and may include additional per-DB keys the user owns.
173    /// The dispatch path for `Authenticated`/`AuthenticatedDb` requests
174    /// validates the identity hint against this set and gates against the
175    /// resulting *acting* pubkey, so a single connection can drive ops on
176    /// databases authored by any key the user has proven they hold.
177    /// `user_uuid` is the per-user scope key for the unified CRDT-state
178    /// cache (see [`crate::backend::CacheScope::User`]).
179    /// `pending_key_challenges` holds outstanding registration challenges,
180    /// keyed by the pubkey the challenge was issued for. Each challenge is
181    /// single-use: the matching `SessionKeyRegister` consumes it whether
182    /// verification succeeds or fails.
183    #[allow(dead_code)] // username surfaces in audit/logging follow-ups
184    Authenticated {
185        username: String,
186        user_uuid: String,
187        login_pubkey: PublicKey,
188        session_keyset: HashSet<PublicKey>,
189        pending_key_challenges: HashMap<PublicKey, Vec<u8>>,
190    },
191}
192
193/// Eidetica service server that listens on a Unix domain socket.
194///
195/// The server wraps a full `Instance` so it can dispatch both storage operations
196/// (via the backend) and write callbacks (via `Instance::put_entry()`'s notification path).
197///
198/// CRDT-state caching lives in the underlying `BackendImpl` (scope-keyed
199/// via [`crate::backend::CacheScope`]); wire handlers route through
200/// `instance.backend()` directly rather than keeping a separate
201/// service-layer cache.
202pub struct ServiceServer {
203    instance: Instance,
204    socket_path: PathBuf,
205    listener: UnixListener,
206    socket_identity: SocketIdentity,
207    token_idle_ttl: Duration,
208    _lock_file: File,
209}
210
211#[derive(Clone, Copy)]
212struct SocketIdentity {
213    device: u64,
214    inode: u64,
215}
216
217impl SocketIdentity {
218    fn from_metadata(metadata: &std::fs::Metadata) -> Self {
219        Self {
220            device: metadata.dev(),
221            inode: metadata.ino(),
222        }
223    }
224
225    fn matches(self, metadata: &std::fs::Metadata) -> bool {
226        self.device == metadata.dev() && self.inode == metadata.ino()
227    }
228}
229
230impl ServiceServer {
231    /// Bind the service socket and return a server ready to accept clients.
232    ///
233    /// # Arguments
234    /// * `instance` - The Instance to serve. The server holds a strong reference.
235    /// * `socket_path` - Path for the Unix domain socket.
236    pub async fn bind(instance: Instance, socket_path: impl Into<PathBuf>) -> crate::Result<Self> {
237        Self::bind_inner(instance, socket_path.into(), TOKEN_IDLE_TTL).await
238    }
239
240    /// Bind with a shorter session-token lifetime for expiry tests.
241    #[cfg(feature = "testing")]
242    pub async fn bind_with_token_idle_ttl_for_test(
243        instance: Instance,
244        socket_path: impl Into<PathBuf>,
245        ttl: Duration,
246    ) -> crate::Result<Self> {
247        Self::bind_inner(instance, socket_path.into(), ttl).await
248    }
249
250    async fn bind_inner(
251        instance: Instance,
252        socket_path: PathBuf,
253        token_idle_ttl: Duration,
254    ) -> crate::Result<Self> {
255        let socket_path = absolute_socket_path(socket_path)?;
256        prepare_parent(&socket_path).await?;
257        let lock_file = acquire_endpoint_lock(&socket_path)?;
258        recover_stale_socket(&socket_path).await?;
259
260        let listener = UnixListener::bind(&socket_path)?;
261        let socket_identity = match prepare_bound_socket(&socket_path) {
262            Ok(identity) => identity,
263            Err(error) => {
264                remove_socket_if_owned(&socket_path, None);
265                return Err(error);
266            }
267        };
268
269        Ok(Self {
270            instance,
271            socket_path,
272            listener,
273            socket_identity,
274            token_idle_ttl,
275            _lock_file: lock_file,
276        })
277    }
278
279    /// Get the socket path.
280    pub fn socket_path(&self) -> &Path {
281        &self.socket_path
282    }
283
284    /// Accept client connections until the shutdown signal is received.
285    ///
286    /// # Arguments
287    /// * `shutdown` - A watch receiver; the server stops when the sender is dropped.
288    pub async fn run(self, mut shutdown: watch::Receiver<()>) -> crate::Result<()> {
289        tracing::info!("Service server listening on {}", self.socket_path.display());
290
291        // Diagnostic per-connection counter — purely for logging. No
292        // registry-based routing needed; each connection's subscriptions
293        // live as per-db callbacks on the Instance, dispatched by the
294        // existing `fire_write_callbacks` path.
295        let next_conn_id = Arc::new(AtomicU64::new(1));
296
297        // Track active per-connection tasks so shutdown actually
298        // disconnects them. Without this the shutdown signal only stops
299        // the accept loop; live handlers keep running until the client
300        // closes its end, which means "shutdown" is half-done — clients
301        // hang on response reads from a daemon they were told had stopped.
302        let mut handlers = JoinSet::new();
303
304        let result = loop {
305            tokio::select! {
306                accept_result = self.listener.accept() => {
307                    match accept_result {
308                        Ok((stream, _addr)) => {
309                            let instance = self.instance.clone();
310                            let token_idle_ttl = self.token_idle_ttl;
311                            let conn_id = next_conn_id.fetch_add(1, Ordering::Relaxed);
312                            handlers.spawn(async move {
313                                if let Err(e) = handle_connection(stream, instance, conn_id, token_idle_ttl).await {
314                                    tracing::debug!(conn_id, "Connection handler error: {e}");
315                                }
316                            });
317                        }
318                        Err(e) => break Err(e.into()),
319                    }
320                }
321                // Reap finished handlers as they complete. Without this arm
322                // the JoinSet only drains at shutdown, so every connection the
323                // daemon has ever served keeps its task entry alive for the
324                // daemon's lifetime.
325                _ = handlers.join_next(), if !handlers.is_empty() => {}
326                _ = shutdown.changed() => {
327                    tracing::info!("Service server shutting down");
328                    break Ok(());
329                }
330            }
331        };
332
333        // Abort live handlers. Each aborted task drops its `UnixStream`
334        // halves, which the client observes as a clean EOF on its read
335        // side — the reader task exits, sets `closed`, and any in-flight
336        // or subsequent `request()` surfaces `ConnectionAborted`.
337        handlers.abort_all();
338        // Drain to completion so the tempdir-owned socket file isn't
339        // pulled out from under any handler that hasn't yet observed
340        // the abort.
341        while handlers.join_next().await.is_some() {}
342
343        result
344    }
345}
346
347const PARENT_MODE: u32 = 0o700;
348const SOCKET_MODE: u32 = 0o660;
349const STALE_PROBE_TIMEOUT: Duration = Duration::from_millis(250);
350
351fn effective_uid() -> u32 {
352    unsafe { libc::geteuid() }
353}
354
355fn absolute_socket_path(path: PathBuf) -> std::io::Result<PathBuf> {
356    let path = if path.is_absolute() {
357        path
358    } else {
359        std::env::current_dir()?.join(path)
360    };
361    if path.file_name().is_none() || path.parent().is_none() {
362        return Err(std::io::Error::new(
363            std::io::ErrorKind::InvalidInput,
364            format!(
365                "service endpoint {} has no parent or file name",
366                path.display()
367            ),
368        ));
369    }
370    Ok(path)
371}
372
373async fn prepare_parent(socket_path: &Path) -> crate::Result<()> {
374    let parent = socket_path
375        .parent()
376        .expect("absolute socket path has a parent");
377    let mut current = PathBuf::new();
378
379    for component in parent.components() {
380        match component {
381            Component::RootDir | Component::Prefix(_) => {
382                current.push(component.as_os_str());
383                continue;
384            }
385            Component::CurDir => continue,
386            Component::ParentDir => {
387                return Err(std::io::Error::new(
388                    std::io::ErrorKind::InvalidInput,
389                    format!("service endpoint parent {} contains '..'", parent.display()),
390                )
391                .into());
392            }
393            Component::Normal(name) => current.push(name),
394        }
395
396        match std::fs::symlink_metadata(&current) {
397            Ok(metadata) => validate_directory_component(&current, &metadata)?,
398            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
399                let mut builder = tokio::fs::DirBuilder::new();
400                builder.mode(PARENT_MODE);
401                match builder.create(&current).await {
402                    Ok(()) => {
403                        tokio::fs::set_permissions(
404                            &current,
405                            std::fs::Permissions::from_mode(PARENT_MODE),
406                        )
407                        .await?;
408                    }
409                    Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
410                    Err(error) => return Err(error.into()),
411                }
412                let metadata = std::fs::symlink_metadata(&current)?;
413                validate_directory_component(&current, &metadata)?;
414            }
415            Err(error) => return Err(error.into()),
416        }
417    }
418
419    let metadata = std::fs::symlink_metadata(parent)?;
420    let mode = metadata.permissions().mode();
421    if metadata.uid() != effective_uid() || mode & 0o022 != 0 {
422        return Err(std::io::Error::new(
423            std::io::ErrorKind::PermissionDenied,
424            format!(
425                "service endpoint parent {} must be owned by the daemon user and not writable by group or others",
426                parent.display()
427            ),
428        )
429        .into());
430    }
431    Ok(())
432}
433
434fn validate_directory_component(path: &Path, metadata: &std::fs::Metadata) -> crate::Result<()> {
435    if !metadata.file_type().is_dir() {
436        return Err(std::io::Error::new(
437            std::io::ErrorKind::PermissionDenied,
438            format!(
439                "service endpoint path component {} must be a directory, not a symlink or file",
440                path.display()
441            ),
442        )
443        .into());
444    }
445    Ok(())
446}
447
448fn lock_path(socket_path: &Path) -> PathBuf {
449    let mut path = socket_path.as_os_str().to_owned();
450    path.push(".lock");
451    PathBuf::from(path)
452}
453
454fn acquire_endpoint_lock(socket_path: &Path) -> crate::Result<File> {
455    let path = lock_path(socket_path);
456    let file = OpenOptions::new()
457        .read(true)
458        .write(true)
459        .create(true)
460        .truncate(false)
461        .mode(0o600)
462        .open(&path)?;
463    if file.metadata()?.permissions().mode() & 0o777 != 0o600 {
464        file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
465    }
466    file.try_lock().map_err(|error| match error {
467        TryLockError::WouldBlock => std::io::Error::new(
468            std::io::ErrorKind::AddrInUse,
469            format!(
470                "service endpoint {} is already owned",
471                socket_path.display()
472            ),
473        ),
474        TryLockError::Error(error) => error,
475    })?;
476    Ok(file)
477}
478
479async fn recover_stale_socket(socket_path: &Path) -> crate::Result<()> {
480    let metadata = match tokio::fs::symlink_metadata(socket_path).await {
481        Ok(metadata) => metadata,
482        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
483        Err(error) => return Err(error.into()),
484    };
485    if !metadata.file_type().is_socket() {
486        return Err(std::io::Error::new(
487            std::io::ErrorKind::AddrInUse,
488            format!(
489                "refusing to replace non-socket service endpoint {}",
490                socket_path.display()
491            ),
492        )
493        .into());
494    }
495
496    match tokio::time::timeout(
497        STALE_PROBE_TIMEOUT,
498        tokio::net::UnixStream::connect(socket_path),
499    )
500    .await
501    {
502        Err(_) => Err(std::io::Error::new(
503            std::io::ErrorKind::TimedOut,
504            format!(
505                "timed out probing service endpoint {}",
506                socket_path.display()
507            ),
508        )
509        .into()),
510        Ok(Ok(_)) => Err(std::io::Error::new(
511            std::io::ErrorKind::AddrInUse,
512            format!("service endpoint {} is live", socket_path.display()),
513        )
514        .into()),
515        Ok(Err(error))
516            if matches!(
517                error.kind(),
518                std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound
519            ) =>
520        {
521            tokio::fs::remove_file(socket_path).await?;
522            Ok(())
523        }
524        Ok(Err(error)) => Err(error.into()),
525    }
526}
527
528fn prepare_bound_socket(socket_path: &Path) -> crate::Result<SocketIdentity> {
529    std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(SOCKET_MODE))?;
530    let metadata = std::fs::symlink_metadata(socket_path)?;
531    if !metadata.file_type().is_socket()
532        || metadata.uid() != effective_uid()
533        || metadata.permissions().mode() & 0o777 != SOCKET_MODE
534    {
535        return Err(std::io::Error::other(format!(
536            "bound service endpoint has incorrect type, owner, or permissions: {}",
537            socket_path.display()
538        ))
539        .into());
540    }
541    Ok(SocketIdentity::from_metadata(&metadata))
542}
543
544fn remove_socket_if_owned(socket_path: &Path, identity: Option<SocketIdentity>) {
545    let Ok(metadata) = std::fs::symlink_metadata(socket_path) else {
546        return;
547    };
548    if metadata.file_type().is_socket()
549        && identity.is_none_or(|expected| expected.matches(&metadata))
550    {
551        let _ = std::fs::remove_file(socket_path);
552    }
553}
554
555impl Drop for ServiceServer {
556    fn drop(&mut self) {
557        remove_socket_if_owned(&self.socket_path, Some(self.socket_identity));
558    }
559}
560
561/// Handle a single client connection.
562///
563/// I/O is split across two tasks:
564///
565/// - **Writer task** (spawned below) owns the `WriteHalf` and drains an
566///   `mpsc::UnboundedReceiver<ServerFrame>` into `write_frame` in
567///   submission order. Responses (dispatched from the request loop) and
568///   server-pushed notifications (from subscribed per-db callbacks both
569///   capture clones of the same `frame_tx`, so a single connection-local
570///   order is preserved.
571///
572/// - **This task** owns the `ReadHalf`, runs the auth/request loop, and
573///   sends `ServerFrame::Response(...)` into the channel. It also holds
574///   a [`ConnectionGuard`] whose Drop unregisters every subscription this
575///   connection made on every exit path (clean EOF, error, or panic).
576///
577/// Handshake still runs inline on the read side first; the writer task is
578/// not started until the handshake succeeds, so a version-mismatch close
579/// uses the simpler inline writer path.
580async fn handle_connection(
581    stream: tokio::net::UnixStream,
582    instance: Instance,
583    conn_id: ConnectionId,
584    token_idle_ttl: Duration,
585) -> crate::Result<()> {
586    let (mut reader, mut writer) = tokio::io::split(stream);
587
588    // 1. Read and validate handshake (inline writer, no channel yet).
589    let handshake: crate::service::protocol::Handshake = match read_frame(&mut reader).await? {
590        Some(h) => h,
591        None => return Ok(()), // Client disconnected before handshake
592    };
593
594    if handshake.protocol_version != PROTOCOL_VERSION {
595        // Send error ack and close
596        let ack = HandshakeAck {
597            protocol_version: PROTOCOL_VERSION,
598        };
599        write_frame(&mut writer, &ack).await?;
600        return Err(crate::Error::Io(std::io::Error::new(
601            std::io::ErrorKind::InvalidData,
602            format!(
603                "Protocol version mismatch: client={}, server={}",
604                handshake.protocol_version, PROTOCOL_VERSION
605            ),
606        )));
607    }
608
609    // Send handshake ack
610    let ack = HandshakeAck {
611        protocol_version: PROTOCOL_VERSION,
612    };
613    write_frame(&mut writer, &ack).await?;
614
615    // 2. Spin up the per-connection writer task.
616    //
617    // TODO(backpressure): this channel is `unbounded`, so a stalled client
618    // reader buffers frames in daemon memory without limit. Socket access is
619    // fully trusted, so an unresponsive reader is a client bug rather than an
620    // adversarial input. Before serving untrusted clients this
621    // needs a bound + drop policy. Complication: this single channel
622    // carries both `Response` and `Notification` frames. Naive bounding
623    // is wrong — dropping a `Response` breaks request/response
624    // correlation. Two viable shapes for the follow-up:
625    //
626    //   a) **Split**: separate `response_tx` (bounded, block-send) from
627    //      `notification_tx` (bounded, drop-oldest); writer `select!`s
628    //      with response priority. Cleanest semantics but touches every
629    //      `frame_tx.clone()` site and the cleanup-ordering comment at
630    //      the end of this function needs migration.
631    //
632    //   b) **Variant-aware drop**: bound the single channel; on full,
633    //      `try_send` returns `Full(frame)`; reject if Notification,
634    //      block if Response. Less plumbing, policy duplicated per send
635    //      site.
636    //
637    // Under the cursor-only `Notification::DatabaseWrite` shape (no entry
638    // bodies, just `previous_tips`/`post_tips`), drops are *recoverable*:
639    // a subscriber that misses event N still sees event N+1's `post_tips`
640    // and calls `ids_added(cursor, post_tips)` to catch up. That makes
641    // drop-oldest the right policy and considerably softens the cost of
642    // dropping at all — but the work still needs doing for prod.
643    let (frame_tx, mut frame_rx) = mpsc::unbounded_channel::<ServerFrame>();
644    let writer_task = tokio::spawn(async move {
645        while let Some(frame) = frame_rx.recv().await {
646            if let Err(e) = write_frame(&mut writer, &frame).await {
647                tracing::debug!(conn_id, "Connection writer error: {e}");
648                break;
649            }
650        }
651    });
652
653    let ctx = Arc::new(ConnectionContext {
654        conn_id,
655        tx: frame_tx.clone(),
656        instance: instance.clone(),
657        subscribed: std::sync::Mutex::new(HashMap::new()),
658        staging: std::sync::Mutex::new(HashMap::new()),
659        token_idle_ttl,
660    });
661    let guard = ConnectionGuard { ctx: ctx.clone() };
662
663    // 3. Request/response loop with per-connection auth state. Responses
664    //    travel through the writer channel as `ServerFrame::Response`.
665    let mut state = ConnectionState::PreAuth;
666    let loop_result: crate::Result<()> = async {
667        loop {
668            let request: ServiceRequest = match read_frame(&mut reader).await? {
669                Some(req) => req,
670                None => break, // Clean EOF
671            };
672
673            let response = dispatch(&instance, &mut state, &ctx, request).await;
674            if frame_tx
675                .send(ServerFrame::Response(Box::new(response)))
676                .is_err()
677            {
678                // Writer task has exited; nothing more we can send.
679                break;
680            }
681        }
682        Ok(())
683    }
684    .await;
685
686    // Cleanup ordering matters — every clone of `frame_tx` must be
687    // dropped before `writer_task.await` can complete (the task is
688    // waiting on `frame_rx.recv()`, which only returns `None` when no
689    // senders remain):
690    //
691    // 1. Drop the local `frame_tx` (this fn's own clone).
692    // 2. Drop `guard`, which runs `ConnectionGuard::drop` and calls
693    //    `Instance::remove_write_callback(tree_id, id)` for every
694    //    subscription this connection registered. Each removed callback
695    //    Arc drops its captured `tx` clone. (No-op when nothing was
696    //    subscribed — e.g. handshake-then-malformed-frame paths.)
697    // 3. Drop `ctx` — releases `ctx.tx`, the last sender clone held
698    //    on the request-loop side.
699    // 4. The writer task's `recv()` returns `None`; the task exits and
700    //    `writer_task.await` finishes.
701    //
702    // Note: a callback dispatch in flight when step 2 runs holds its
703    // own Arc to the closure (from `fire_write_callbacks`'s snapshot).
704    // It can still send a final notification through its captured `tx`
705    // before that Arc is released; the writer task will drain that
706    // frame before exiting in step 4.
707    drop(frame_tx);
708    drop(guard);
709    drop(ctx);
710    let _ = writer_task.await;
711
712    // Session teardown: subscription cleanup ran via the guard's Drop.
713    // The unified CRDT-state cache lives in `BackendImpl` and is bounded
714    // by its own eviction policy (byte-bounded LRU on the in-memory
715    // backend; disk-bounded on SQL). Per-user cache slots survive
716    // disconnect intentionally so a reconnecting client recovers
717    // materialized state from tier 2 without recomputing from entries.
718
719    loop_result
720}
721
722/// Dispatch a service request to the appropriate Instance/Backend method.
723async fn dispatch(
724    instance: &Instance,
725    state: &mut ConnectionState,
726    ctx: &ConnectionContext,
727    request: ServiceRequest,
728) -> ServiceResponse {
729    match dispatch_inner(instance, state, ctx, request).await {
730        Ok(resp) => resp,
731        Err(e) => ServiceResponse::Error(ServiceError::from(&e)),
732    }
733}
734
735/// Inner dispatch that returns Result for ergonomic error handling.
736async fn dispatch_inner(
737    instance: &Instance,
738    state: &mut ConnectionState,
739    ctx: &ConnectionContext,
740    request: ServiceRequest,
741) -> crate::Result<ServiceResponse> {
742    for token in ctx.cleanup_expired() {
743        let _ = instance.backend().abort_store_state(token).await;
744    }
745    match request {
746        // === Pre-auth: login handshake ===
747        ServiceRequest::TrustedLoginUser { username } => {
748            handle_trusted_login_user(instance, state, username).await
749        }
750        ServiceRequest::TrustedLoginProve { signature } => {
751            handle_trusted_login_prove(state, &signature)
752        }
753
754        // === Pre-auth: server identity ===
755        ServiceRequest::GetInstanceMetadata => {
756            let metadata = instance.backend().get_instance_metadata().await?;
757            Ok(ServiceResponse::InstanceMetadata(metadata))
758        }
759
760        // === Post-auth: extend the session keyset ===
761        ServiceRequest::SessionKeyChallenge { pubkey } => {
762            handle_session_key_challenge(state, pubkey)
763        }
764        ServiceRequest::SessionKeyRegister { pubkey, signature } => {
765            handle_session_key_register(state, pubkey, &signature)
766        }
767
768        // === Authenticated storage operations ===
769        //
770        // Gate 1: the connection must have completed `TrustedLogin*`. Gate 2:
771        // the per-tree permission gate. Every `DatabaseOp` carries its target
772        // `root_id` explicitly, so the gate is *unconditional* — there is no
773        // tree-less op to fall through it — with two exceptions handled below
774        // (`SubmitSignedEntry`, verification-gated; `SetInstanceMetadata`,
775        // gated against the server-known `_databases`, not the request root).
776        ServiceRequest::AuthenticatedDb(inner) => {
777            let (login_pubkey, keyset_snapshot, session_user_uuid) = match state {
778                ConnectionState::Authenticated {
779                    login_pubkey,
780                    session_keyset,
781                    user_uuid,
782                    ..
783                } => (
784                    login_pubkey.clone(),
785                    session_keyset.clone(),
786                    user_uuid.clone(),
787                ),
788                _ => {
789                    return Err(crate::Error::Auth(Box::new(
790                        AuthError::InvalidAuthConfiguration {
791                            reason: format!(
792                                "database operation requires an authenticated connection; complete TrustedLogin* first (op: {:?})",
793                                inner.op
794                            ),
795                        },
796                    )));
797                }
798            };
799
800            let AuthenticatedDbRequest {
801                root_id,
802                identity,
803                op,
804            } = *inner;
805
806            // Submit is verification-gated, not session-gated.
807            //
808            // Reads are session-gated (confidentiality boundary); submits
809            // are verification-gated (integrity boundary). `SubmitSignedEntry`
810            // requires only an *authenticated* connection (gate 1, the
811            // `ConnectionState::Authenticated` match above, still applies).
812            // Which tree the entry belongs to, and whether its signer may
813            // write that tree, is decided by the server's own verification
814            // pass in the handler (store `Unverified`, then
815            // `Database::open(...).verify_with_source(...)`) against the tree's *real*
816            // pinned auth lineage — not by who holds the socket. An attacker
817            // without a key the tree's auth grants cannot produce a
818            // `Verified` entry, and unverified junk is excluded from every
819            // default read by the frontier cut, so the per-tree session gate
820            // adds no correctness or isolation property here; it only blocks
821            // a legitimate transporter (e.g. an admin session carrying a
822            // user-signed genesis). See the verification-gated-submit design
823            // doc for the full threat analysis.
824            let is_submit = matches!(op, DatabaseOp::SubmitSignedEntry { .. });
825
826            // `SetInstanceMetadata` rewrites the daemon's pointers to its own
827            // system DBs. It is gated against `_databases` (a server-known
828            // tree), not the request's `root_id`, so an instance admin — by
829            // construction a user with Admin on `_databases` via the
830            // first-user bootstrap — is required. Fail closed (require_existing
831            // = true): `_databases` always exists on an initialized daemon and
832            // this is never a creation flow, so the create-flow passthrough
833            // must not apply (it would let any authenticated user rewrite
834            // system-DB pointers if `_databases` were ever unreadable).
835            let is_set_metadata = matches!(op, DatabaseOp::SetInstanceMetadata { .. });
836
837            // Submit accepts any identity hint (admin transports user-signed
838            // entries); every other op resolves an acting pubkey from the
839            // keyset and gates per-tree against it.
840            let acting_pubkey = if is_submit {
841                // Use the hint if it parses as a pubkey (for submit metadata),
842                // otherwise fall back to login_pubkey — submit doesn't gate
843                // on this value.
844                identity
845                    .hint()
846                    .pubkey
847                    .clone()
848                    .unwrap_or_else(|| login_pubkey.clone())
849            } else {
850                resolve_acting_pubkey(&identity, &login_pubkey, &keyset_snapshot)?
851            };
852
853            // Per-tree permission gate. Unconditional for every op *except*
854            // submit (verification in the handler is its boundary) and
855            // set-metadata (gated against `_databases` below). Create-flow
856            // passthrough (false) for the rest, so a not-yet-propagated tree is
857            // waved through and database creation works.
858            if is_set_metadata {
859                gate_tree_permission(
860                    instance,
861                    &acting_pubkey,
862                    &identity,
863                    instance.databases_db_id(),
864                    Permission::Admin(0),
865                    true,
866                )
867                .await?;
868            } else if !is_submit {
869                gate_tree_permission(
870                    instance,
871                    &acting_pubkey,
872                    &identity,
873                    &root_id,
874                    op.required_permission(),
875                    false,
876                )
877                .await?;
878            }
879
880            dispatch_database_op(
881                instance,
882                ctx,
883                &acting_pubkey,
884                &identity,
885                &session_user_uuid,
886                root_id,
887                op,
888            )
889            .await
890        }
891    }
892}
893
894/// Resolve the *acting* pubkey for a session-gated op.
895///
896/// The identity hint, when present, must be in the connection's session
897/// keyset (proof of possession registered via `SessionKeyChallenge` /
898/// `SessionKeyRegister`, or established at login time). Returning the hint
899/// as the acting pubkey lets the per-tree gate check the actual key the
900/// caller wants to act as, not the connection-wide login key.
901///
902/// An absent hint defaults to the login pubkey — matches the pre-keyset
903/// behavior where every op acted as the login identity.
904fn resolve_acting_pubkey(
905    identity: &SigKey,
906    login_pubkey: &PublicKey,
907    session_keyset: &HashSet<PublicKey>,
908) -> crate::Result<PublicKey> {
909    match &identity.hint().pubkey {
910        Some(claimed) if session_keyset.contains(claimed) => Ok(claimed.clone()),
911        Some(claimed) => Err(crate::Error::Auth(Box::new(
912            AuthError::SigningKeyMismatch {
913                reason: format!(
914                    "request identity claims pubkey '{claimed}' but it is not in the session keyset; \
915                     register it first via SessionKeyChallenge/SessionKeyRegister"
916                ),
917            },
918        ))),
919        None => Ok(login_pubkey.clone()),
920    }
921}
922
923/// Dispatch a Database-level op against the server's local `Database`.
924///
925/// Additive sibling of `dispatch_backend_op`. The caller has already verified
926/// the session identity and run the unconditional per-tree permission gate on
927/// `root_id`. Because the server runs the `Database` layer here, verify-on-read
928/// and the Verified frontier are server-side **by construction**.
929async fn dispatch_database_op(
930    instance: &Instance,
931    ctx: &ConnectionContext,
932    acting_pubkey: &PublicKey,
933    identity: &SigKey,
934    user_uuid: &str,
935    root_id: ID,
936    op: DatabaseOp,
937) -> crate::Result<ServiceResponse> {
938    match op {
939        DatabaseOp::ResolveStoreState { request } => {
940            let request = session_store_state_request(request, user_uuid, &root_id)?;
941            let view = match instance.backend().resolve_store_state(&request).await? {
942                Some(view) => Some(view),
943                // A client asks with its own scope; the daemon's own
944                // materialized state is shared, so fall back to it rather than
945                // rebuilding per user.
946                None => {
947                    let shared = StoreStateRequest {
948                        scope: CacheScope::Shared,
949                        ..request.clone()
950                    };
951                    instance.backend().resolve_store_state(&shared).await?
952                }
953            };
954            let token = view.map(|view| {
955                let token = random_token();
956                ctx.staging.lock().unwrap().insert(
957                    token.clone(),
958                    SessionStaging {
959                        backend: StagingToken {
960                            namespace_id: view.namespace_id,
961                            target: request.clone(),
962                        },
963                        user_uuid: user_uuid.to_string(),
964                        database: request.database.clone(),
965                        published: true,
966                        chunks: HashMap::new(),
967                        last_used: Instant::now(),
968                    },
969                );
970                token
971            });
972            Ok(ServiceResponse::RecordView(token))
973        }
974        DatabaseOp::BeginStoreStateStaging { request } => {
975            let request = session_store_state_request(request, user_uuid, &root_id)?;
976            let backend = instance
977                .backend()
978                .begin_store_state_staging(request.clone())
979                .await?;
980            let token = random_token();
981            ctx.staging.lock().unwrap().insert(
982                token.clone(),
983                SessionStaging {
984                    backend,
985                    user_uuid: user_uuid.to_string(),
986                    database: request.database,
987                    published: false,
988                    chunks: HashMap::new(),
989                    last_used: Instant::now(),
990                },
991            );
992            Ok(ServiceResponse::Token(token))
993        }
994        DatabaseOp::StageStoreStateRecords {
995            token,
996            chunk_id,
997            records,
998        } => {
999            let digest = serde_json::to_vec(&records)?;
1000            if digest.len() > crate::service::protocol::MAX_RECORD_CHUNK_BYTES as usize {
1001                return Err(crate::backend::BackendError::RecordTooLarge {
1002                    encoded_bytes: digest.len(),
1003                }
1004                .into());
1005            }
1006            let records = records
1007                .into_iter()
1008                .collect::<crate::backend::RecordMutations>();
1009            let (backend, duplicate) =
1010                session_staging(ctx, user_uuid, &root_id, &token, chunk_id, &digest)?;
1011            if duplicate {
1012                return Ok(ServiceResponse::Ok);
1013            }
1014            if let Err(error) = instance
1015                .backend()
1016                .stage_store_state_records(&backend, records)
1017                .await
1018            {
1019                if let Some(value) = ctx.staging.lock().unwrap().get_mut(&token) {
1020                    value.chunks.remove(&chunk_id);
1021                }
1022                return Err(error);
1023            }
1024            Ok(ServiceResponse::Ok)
1025        }
1026        DatabaseOp::PublishStoreState { token } => {
1027            let staged = ctx
1028                .staging
1029                .lock()
1030                .unwrap()
1031                .remove(&token)
1032                .filter(|value| value.user_uuid == user_uuid && value.database == root_id)
1033                .ok_or(crate::backend::BackendError::InvalidStoreStateStagingToken)?;
1034            let backend_token = staged.backend;
1035            let view = match instance
1036                .backend()
1037                .publish_store_state(backend_token.clone())
1038                .await
1039            {
1040                Ok(view) => view,
1041                Err(error) => {
1042                    let _ = instance.backend().abort_store_state(backend_token).await;
1043                    return Err(error);
1044                }
1045            };
1046            let view_token = random_token();
1047            let database = root_id.clone();
1048            ctx.staging.lock().unwrap().insert(
1049                view_token.clone(),
1050                SessionStaging {
1051                    backend: StagingToken {
1052                        namespace_id: view.namespace_id,
1053                        target: StoreStateRequest {
1054                            database: database.clone(),
1055                            store: String::new(),
1056                            lifecycle: StoreStateLifecycle::Derived,
1057                            scope: CacheScope::User(user_uuid.to_string()),
1058                            projection: crate::backend::ProjectionDescriptor {
1059                                name: String::new(),
1060                                version: 0,
1061                            },
1062                            source_key: Vec::new(),
1063                        },
1064                    },
1065                    user_uuid: user_uuid.to_string(),
1066                    database,
1067                    published: true,
1068                    chunks: HashMap::new(),
1069                    last_used: Instant::now(),
1070                },
1071            );
1072            Ok(ServiceResponse::RecordView(Some(view_token)))
1073        }
1074        DatabaseOp::AbortStoreState { token } => {
1075            let staged = {
1076                let mut staging = ctx.staging.lock().unwrap();
1077                if staging
1078                    .get(&token)
1079                    .is_some_and(|value| value.user_uuid != user_uuid || value.database != root_id)
1080                {
1081                    return Err(crate::backend::BackendError::InvalidStoreStateStagingToken.into());
1082                }
1083                staging.remove(&token)
1084            };
1085            if let Some(staged) = staged {
1086                instance.backend().abort_store_state(staged.backend).await?;
1087            }
1088            Ok(ServiceResponse::Ok)
1089        }
1090        DatabaseOp::StoreStateRecordGet { view, key } => {
1091            let namespace_id = {
1092                let mut staged = ctx.staging.lock().unwrap();
1093                staged
1094                    .get_mut(&view)
1095                    .filter(|value| {
1096                        value.user_uuid == user_uuid && value.database == root_id && value.published
1097                    })
1098                    .map(|value| {
1099                        value.last_used = Instant::now();
1100                        value.backend.namespace_id.clone()
1101                    })
1102                    .ok_or(crate::backend::BackendError::InvalidStoreStateView)?
1103            };
1104            let record = instance
1105                .backend()
1106                .store_state_record_get(&crate::backend::RecordView { namespace_id }, &key)
1107                .await?;
1108            ensure_record_fits(record.as_ref())?;
1109            Ok(ServiceResponse::Record(record))
1110        }
1111        DatabaseOp::StoreStateRecordScan {
1112            view,
1113            range,
1114            after,
1115            max_records,
1116            max_encoded_bytes,
1117        } => {
1118            let backend = {
1119                let mut staged = ctx.staging.lock().unwrap();
1120                staged
1121                    .get_mut(&view)
1122                    .filter(|value| {
1123                        value.user_uuid == user_uuid && value.database == root_id && value.published
1124                    })
1125                    .map(|value| {
1126                        value.last_used = Instant::now();
1127                        value.backend.clone()
1128                    })
1129                    .ok_or(crate::backend::BackendError::InvalidStoreStateView)?
1130            };
1131            let page = bounded_page(
1132                instance
1133                    .backend()
1134                    .store_state_record_scan(
1135                        &crate::backend::RecordView {
1136                            namespace_id: backend.namespace_id,
1137                        },
1138                        &range,
1139                        after.as_deref(),
1140                        max_records as usize,
1141                    )
1142                    .await?,
1143                max_encoded_bytes,
1144            )?;
1145            Ok(ServiceResponse::RecordPage(page))
1146        }
1147
1148        DatabaseOp::GetEntry { id } => {
1149            let entry = instance.backend().get(&id).await?;
1150            // Post-fetch owning-tree Read gate: a raw entry id carries no
1151            // inline tree, so the pre-dispatch gate (which keys on `root_id`)
1152            // could not cover the entry's real owning tree.
1153            gate_entry_read(instance, acting_pubkey, identity, &entry).await?;
1154            Ok(ServiceResponse::Entry(entry))
1155        }
1156
1157        DatabaseOp::GetVerifiedTips => {
1158            // The server runs the Database layer, so `snapshot()` returns the
1159            // Verified frontier by construction — no client-side verify, no
1160            // remote-detection heuristic.
1161            let db = Database::open(instance, &root_id).await?;
1162            let snapshot = db.snapshot().await?;
1163            Ok(ServiceResponse::Ids(snapshot))
1164        }
1165
1166        DatabaseOp::SubmitSignedEntry { entry } => {
1167            let previous_tips = instance.snapshot(&root_id).await?;
1168            // The client signed this entry; the server does NOT trust its
1169            // claimed validity. Store it `Unverified`, then run our OWN
1170            // verification pass against the entry's pinned settings. A
1171            // poisoned entry never reaches `Verified` and is excluded from
1172            // every default read by the frontier cut — D1 is closed by
1173            // construction here, not by gate-hardening a raw `Put`.
1174            //
1175            // Only settled-state writes trigger an event: the
1176            // `put_entry(.., Unverified, ..)` is a no-fire path by
1177            // design (see `Instance::put_entry`), and the verification pass
1178            // fires its own batched `Verified` event for any entries
1179            // the pass settles. The handler just chains the two — no
1180            // extra fire bookkeeping here. Note this gates the trigger
1181            // only; the event's raw-frontier cursors can still bracket
1182            // an unsettled tip (see `Notification` rustdoc).
1183            instance
1184                .put_entry(
1185                    &root_id,
1186                    VerificationStatus::Unverified,
1187                    *entry,
1188                    WriteSource::Local,
1189                )
1190                .await?;
1191            Database::open(instance, &root_id)
1192                .await?
1193                .verify_with_source(WriteSource::Local, Some(previous_tips))
1194                .await?;
1195            Ok(ServiceResponse::Ok)
1196        }
1197
1198        DatabaseOp::BeginTransaction { stores, scope } => {
1199            // Single-sourced: both this handler and the Phase-3 remote seam
1200            // call `Database::transaction_context`, so `Transaction::commit`'s
1201            // build-sign path has one source of truth.
1202            let db = Database::open(instance, &root_id).await?;
1203            let ctx = db.transaction_context(&stores, scope).await?;
1204            Ok(ServiceResponse::TransactionContext(ctx))
1205        }
1206
1207        DatabaseOp::GetStoreState { store } => {
1208            // Server-materialized merged state (unencrypted stores only).
1209            // Encrypted stores must use GetStoreEntries instead — the
1210            // ephemeral transaction here has no encryptor, and Doc
1211            // deserialization would fail on ciphertext.
1212            let db = Database::open(instance, &root_id).await?;
1213            let value = db.get_store_state(&store).await?;
1214            Ok(ServiceResponse::CrdtValue(value))
1215        }
1216
1217        DatabaseOp::GetStoreEntries { store, tips, scope } => {
1218            // Universal primitive (encrypted + unencrypted): returns raw
1219            // Entry records with opaque data in canonical CRDT replay order.
1220            // For encrypted stores the client decrypts+merges locally.
1221            let db = Database::open(instance, &root_id).await?;
1222            let entries = db.get_store_entries(&store, &tips, scope).await?;
1223            Ok(ServiceResponse::Entries(entries))
1224        }
1225
1226        DatabaseOp::GetStoreTipsUpToEntries { store, up_to } => {
1227            let db = Database::open(instance, &root_id).await?;
1228            let boundary = crate::Snapshot::from(up_to);
1229            let snapshot = db
1230                .ops()
1231                .store_snapshot_at(&root_id, &store, &boundary)
1232                .await?;
1233            Ok(ServiceResponse::Ids(snapshot))
1234        }
1235
1236        DatabaseOp::ComputeMergeState { store, entry_ids } => {
1237            let db = Database::open(instance, &root_id).await?;
1238            let slice = db
1239                .ops()
1240                .compute_merge_state(&root_id, &store, &entry_ids)
1241                .await?;
1242            Ok(ServiceResponse::MergeState(MergeState {
1243                merge_base: slice.merge_base,
1244                path: slice.path,
1245            }))
1246        }
1247
1248        DatabaseOp::SetInstanceMetadata { metadata } => {
1249            // Admin-on-`_databases` gate already ran in the dispatcher (against
1250            // the server-known system tree, not `root_id`).
1251            instance.backend().set_instance_metadata(&metadata).await?;
1252            Ok(ServiceResponse::Ok)
1253        }
1254
1255        DatabaseOp::SubscribeWrites { tips } => {
1256            // Per-tree Read gate already ran in the dispatcher.
1257            //
1258            // Subscription is just a per-db callback registered against the
1259            // daemon's Instance. The callback's body pushes a notification
1260            // frame into this connection's writer channel; the daemon's
1261            // existing `fire_write_callbacks` dispatch handles fan-out by
1262            // walking the per-tree callback list. No separate registry or
1263            // global-publisher hook needed.
1264            //
1265            // Idempotent: a tree this connection has already subscribed to
1266            // is a no-op (we'd otherwise register a second callback that
1267            // pushes a duplicate frame on every write). Take the lock
1268            // briefly to early-out, then release before any await — the
1269            // std::Mutex isn't `Send` and can't cross an await point.
1270            //
1271            // Under the current dispatch shape the per-connection request
1272            // loop is single-threaded (one frame → one dispatch → one
1273            // response), so a second `SubscribeWrites` for the same tree
1274            // on the same connection can only arrive *after* the first
1275            // call has fully completed and recorded its subscription. The
1276            // post-await Entry-API guard below is therefore **defensive
1277            // against a future shape change** (per-connection parallel
1278            // dispatch, or another path that takes `ctx` and registers
1279            // callbacks concurrently) rather than fixing a present race.
1280            // Cheap to keep — one Entry-API call plus a single
1281            // `remove_write_callback` in the unreachable Occupied arm.
1282            {
1283                let subs = ctx.subscribed_lock();
1284                if subs.contains_key(&root_id) {
1285                    return Ok(ServiceResponse::Ok);
1286                }
1287            }
1288            let tx = ctx.tx.clone();
1289            // Capture the initial cursor and register the callback atomically
1290            // under the tree lock. The fire path (`put_entry` /
1291            // `Database::verify`) holds this same lock while advancing cursors,
1292            // so no write can land between the tips snapshot and the
1293            // registration: the empty-`tips` cursor is exactly the daemon's
1294            // frontier at the instant this subscription begins, with no
1295            // lost-update gap. Safe to hold across the `snapshot` await —
1296            // `Instance::snapshot` reads the raw backend (no tree lock) and,
1297            // unlike the fire path, we spawn/await no user callback under the
1298            // guard, so there is no reentrancy.
1299            let tree_lock = instance.tree_lock(&root_id);
1300            let tree_guard = tree_lock.lock().await;
1301            // Initial cursor: the client's supplied `tips` if non-empty;
1302            // otherwise the daemon's current tips at subscribe-time (the "I have
1303            // no initial state; give me events from now" posture documented on
1304            // the wire variant). The cursor advances per fire as usual.
1305            let initial_tips = if tips.is_empty() {
1306                // Propagate rather than defaulting: an error here would seed
1307                // the daemon-side cursor at `EMPTY`, which is the "replay
1308                // from the beginning" value rather than "start from now".
1309                instance.snapshot(&root_id).await?
1310            } else {
1311                tips
1312            };
1313            let id = instance.register_write_callback(
1314                root_id.clone(),
1315                initial_tips,
1316                move |event, db| {
1317                    // ORDERING INVARIANT — DO NOT add an `.await` before the
1318                    // `tx.send` below. Per-tree notifications are delivered in
1319                    // daemon-canonical order, and that guarantee rests entirely
1320                    // on this send running *synchronously*, in the closure's
1321                    // pre-future prefix. `Instance::spawn_write_callbacks`
1322                    // invokes this closure while the caller holds the tree's
1323                    // `tree_lock` (see `put_entry` / `Database::verify`), and it
1324                    // advances each subscription's cursor in the same critical
1325                    // section. Because the send happens here — before any await,
1326                    // still under that lock, in cursor-advance order — two
1327                    // connections writing the same tree concurrently cannot
1328                    // interleave their frames: writer A's send completes and A
1329                    // drops the lock before writer B can acquire it and send.
1330                    // The returned future is deliberately a no-op; only that
1331                    // (empty) tail is spawned and drained lock-free. Moving the
1332                    // send *into* the future — or awaiting anything before it —
1333                    // would push it out of the locked section onto the runtime's
1334                    // scheduler and let same-tree frames reorder (the bug fixed
1335                    // in "preserve write-notification order under concurrent
1336                    // writers"). `mpsc::UnboundedSender::send` is non-blocking
1337                    // and takes `&self`, which is exactly what makes a
1338                    // synchronous send here possible.
1339                    //
1340                    // Send failure (writer task gone) is silently dropped; the
1341                    // connection is about to be torn down and the guard's Drop
1342                    // will unregister us next.
1343                    //
1344                    // The closure only fires for settled-state writes today
1345                    // (Verified). Unverified writes go through `put_entry`
1346                    // without firing `fire_write_callbacks`, so no notification
1347                    // is ever *triggered* by them — though the cursors shipped
1348                    // here are raw frontiers and can bracket an unsettled tip.
1349                    let frame = ServerFrame::Notification(Notification::DatabaseWrite {
1350                        root_id: db.root_id().clone(),
1351                        previous_tips: event.previous_tips().clone(),
1352                        post_tips: event.post_tips().clone(),
1353                        source: event.source(),
1354                    });
1355                    let _ = tx.send(frame);
1356                    async move { Ok(()) }
1357                },
1358            );
1359            // Cursor captured and callback live; release the tree lock before
1360            // touching the std subscribed-lock below (never nest the two).
1361            drop(tree_guard);
1362            // Re-acquire the lock to record this connection's subscription.
1363            // Defensive Entry-API guard: in the current shape (single-
1364            // threaded per-connection dispatch) the Occupied arm is
1365            // unreachable — see the comment above the early-out check.
1366            // Kept so that a future shape change (parallel dispatch per
1367            // connection, or another concurrent path that takes `ctx`)
1368            // can't leave us with two callbacks pushing duplicate frames
1369            // on every write; the loser drops its just-registered
1370            // callback and the first registration stays.
1371            use std::collections::hash_map::Entry as MapEntry;
1372            let mut subs = ctx.subscribed_lock();
1373            match subs.entry(root_id.clone()) {
1374                MapEntry::Vacant(slot) => {
1375                    slot.insert(id);
1376                }
1377                MapEntry::Occupied(_) => {
1378                    drop(subs);
1379                    instance.remove_write_callback(&root_id, id);
1380                }
1381            }
1382            Ok(ServiceResponse::Ok)
1383        }
1384
1385        DatabaseOp::UnsubscribeWrites => {
1386            // Per-tree Read gate already ran. Idempotent: unsubscribing a
1387            // tree this connection was not subscribed to is a no-op.
1388            let removed = ctx.subscribed_lock().remove(&root_id);
1389            if let Some(id) = removed {
1390                instance.remove_write_callback(&root_id, id);
1391            }
1392            Ok(ServiceResponse::Ok)
1393        }
1394
1395        DatabaseOp::CreateTicket => Ok(ServiceResponse::DatabaseTicket(
1396            crate::user::ticket_locator(instance, &root_id).await?,
1397        )),
1398    }
1399}
1400
1401fn random_token() -> String {
1402    uuid::Uuid::new_v4().to_string()
1403}
1404
1405/// Refuse a record the wire cannot carry, before it is framed.
1406fn ensure_record_fits(record: Option<&Vec<u8>>) -> crate::Result<()> {
1407    if let Some(record) = record {
1408        let encoded = serde_json::to_vec(record)?.len();
1409        if encoded > crate::service::protocol::MAX_RECORD_CHUNK_BYTES as usize {
1410            return Err(crate::backend::BackendError::RecordTooLarge {
1411                encoded_bytes: encoded,
1412            }
1413            .into());
1414        }
1415    }
1416    Ok(())
1417}
1418
1419/// Trim a page to the caller's encoded-byte budget, re-deriving the exclusive
1420/// continuation key from what actually survives. A single record that cannot
1421/// fit under the budget is an error rather than a silently empty page.
1422fn bounded_page(
1423    mut page: crate::backend::RecordPage,
1424    max_encoded_bytes: u32,
1425) -> crate::Result<crate::backend::RecordPage> {
1426    let limit = max_encoded_bytes.min(crate::service::protocol::MAX_RECORD_CHUNK_BYTES) as usize;
1427    let mut size = 2usize;
1428    let mut keep = 0usize;
1429    for record in &page.records {
1430        let record_size = serde_json::to_vec(record)?.len();
1431        if record_size > limit {
1432            return Err(crate::backend::BackendError::RecordTooLarge {
1433                encoded_bytes: record_size,
1434            }
1435            .into());
1436        }
1437        if size + record_size > limit {
1438            break;
1439        }
1440        size += record_size;
1441        keep += 1;
1442    }
1443    if keep < page.records.len() {
1444        page.records.truncate(keep);
1445        page.next = page.records.last().map(|(key, _)| key.clone());
1446    }
1447    Ok(page)
1448}
1449
1450/// Bind a client-supplied Store-state request to the authenticated session.
1451///
1452/// The request travels with its own database and scope, so both are checked
1453/// against the gate the dispatcher already applied: the database must be the
1454/// authorized one, and a client may only address shared state or its own user
1455/// scope. Client-computed state is written under the user scope, so one
1456/// session cannot publish into another's namespace or into the daemon's.
1457fn session_store_state_request(
1458    mut request: StoreStateRequest,
1459    user_uuid: &str,
1460    database: &ID,
1461) -> crate::Result<StoreStateRequest> {
1462    if request.database != *database || request.lifecycle != StoreStateLifecycle::Derived {
1463        return Err(crate::backend::BackendError::InvalidStoreStateStagingToken.into());
1464    }
1465    match &request.scope {
1466        CacheScope::Shared => request.scope = CacheScope::User(user_uuid.to_string()),
1467        CacheScope::User(uuid) if uuid == user_uuid => {}
1468        CacheScope::User(_) => {
1469            return Err(crate::backend::BackendError::InvalidStoreStateStagingToken.into());
1470        }
1471    }
1472    Ok(request)
1473}
1474
1475/// Resolve a session staging token, recording the chunk digest so a retried
1476/// upload is idempotent and a changed one is refused.
1477fn session_staging(
1478    ctx: &ConnectionContext,
1479    user_uuid: &str,
1480    database: &ID,
1481    token: &str,
1482    chunk_id: u64,
1483    digest: &[u8],
1484) -> crate::Result<(StagingToken, bool)> {
1485    let mut staging = ctx.staging.lock().unwrap();
1486    let value = staging
1487        .get_mut(token)
1488        .filter(|value| value.user_uuid == user_uuid && value.database == *database)
1489        .ok_or(crate::backend::BackendError::InvalidStoreStateStagingToken)?;
1490    if let Some(existing) = value.chunks.get(&chunk_id) {
1491        if existing != digest {
1492            return Err(crate::backend::BackendError::InvalidStoreStateStagingToken.into());
1493        }
1494        return Ok((value.backend.clone(), true));
1495    }
1496    value.chunks.insert(chunk_id, digest.to_vec());
1497    value.last_used = Instant::now();
1498    Ok((value.backend.clone(), false))
1499}
1500
1501/// Handle `ServiceRequest::TrustedLoginUser`: look up the user's full record,
1502/// mint a challenge, and move the connection into `AwaitingProof`.
1503async fn handle_trusted_login_user(
1504    instance: &Instance,
1505    state: &mut ConnectionState,
1506    username: String,
1507) -> crate::Result<ServiceResponse> {
1508    // Look up the full `UserInfo`. The encrypted root key + salt ship to the
1509    // client so it can decrypt locally and sign the challenge in the same
1510    // round-trip; the daemon never sees the password or the plaintext key.
1511    // The non-credential fields (user_database_id, status) ride along so the
1512    // client can build the `User` session after proof without a second wire
1513    // read of `_users`. If the lookup fails (no such user, disabled,
1514    // duplicates), drop back to `PreAuth` and bubble the error.
1515    let users_db = instance.users_db().await?;
1516    let (user_uuid, user_info) = match lookup_user_record(&users_db, &username).await {
1517        Ok(v) => v,
1518        Err(e) => {
1519            *state = ConnectionState::PreAuth;
1520            return Err(e);
1521        }
1522    };
1523
1524    let expected_pubkey = user_info.credentials.root_key_id.clone();
1525    let challenge = generate_challenge();
1526    *state = ConnectionState::AwaitingProof {
1527        username,
1528        user_uuid: user_uuid.clone(),
1529        challenge: challenge.clone(),
1530        expected_pubkey,
1531    };
1532    Ok(ServiceResponse::TrustedLoginChallenge {
1533        challenge,
1534        user_uuid,
1535        user_info,
1536    })
1537}
1538
1539/// Handle `ServiceRequest::TrustedLoginProve`: verify the signature against the
1540/// stored challenge and either transition to `Authenticated` or drop back
1541/// to `PreAuth`.
1542fn handle_trusted_login_prove(
1543    state: &mut ConnectionState,
1544    signature: &[u8],
1545) -> crate::Result<ServiceResponse> {
1546    let (username, user_uuid, challenge, expected_pubkey) =
1547        match std::mem::replace(state, ConnectionState::PreAuth) {
1548            ConnectionState::AwaitingProof {
1549                username,
1550                user_uuid,
1551                challenge,
1552                expected_pubkey,
1553            } => (username, user_uuid, challenge, expected_pubkey),
1554            other => {
1555                // Restore the (unexpected) state we just took out so subsequent
1556                // requests see consistent state.
1557                *state = other;
1558                return Err(crate::Error::Io(std::io::Error::new(
1559                    std::io::ErrorKind::InvalidData,
1560                    "TrustedLoginProve received outside of AwaitingProof state",
1561                )));
1562            }
1563        };
1564
1565    match verify_challenge_response(&challenge, signature, &expected_pubkey) {
1566        Ok(()) => {
1567            let mut session_keyset = HashSet::new();
1568            session_keyset.insert(expected_pubkey.clone());
1569            *state = ConnectionState::Authenticated {
1570                username,
1571                user_uuid,
1572                login_pubkey: expected_pubkey,
1573                session_keyset,
1574                pending_key_challenges: HashMap::new(),
1575            };
1576            Ok(ServiceResponse::TrustedLoginOk)
1577        }
1578        Err(e) => {
1579            // Already reset to PreAuth via the mem::replace above.
1580            Err(crate::Error::Auth(Box::new(e)))
1581        }
1582    }
1583}
1584
1585/// Handle `ServiceRequest::SessionKeyChallenge`: mint a single-use challenge
1586/// bound to `pubkey` and stash it in the connection's pending-challenges map.
1587///
1588/// Requires an authenticated connection. A repeat call for the same pubkey
1589/// overwrites the prior challenge — last-issued wins, so a stale challenge
1590/// can't be replayed.
1591fn handle_session_key_challenge(
1592    state: &mut ConnectionState,
1593    pubkey: PublicKey,
1594) -> crate::Result<ServiceResponse> {
1595    match state {
1596        ConnectionState::Authenticated {
1597            pending_key_challenges,
1598            ..
1599        } => {
1600            let challenge = generate_challenge();
1601            pending_key_challenges.insert(pubkey, challenge.clone());
1602            Ok(ServiceResponse::SessionKeyChallenge { challenge })
1603        }
1604        _ => Err(crate::Error::Auth(Box::new(
1605            AuthError::InvalidAuthConfiguration {
1606                reason: "SessionKeyChallenge requires an authenticated connection; \
1607                     complete TrustedLogin* first"
1608                    .to_string(),
1609            },
1610        ))),
1611    }
1612}
1613
1614/// Handle `ServiceRequest::SessionKeyRegister`: verify the signature against
1615/// the matching pending challenge and, on success, add `pubkey` to the
1616/// connection's session keyset.
1617///
1618/// The challenge is consumed (removed) whether verification succeeds or fails,
1619/// so a bad signature can't be retried against the same challenge.
1620fn handle_session_key_register(
1621    state: &mut ConnectionState,
1622    pubkey: PublicKey,
1623    signature: &[u8],
1624) -> crate::Result<ServiceResponse> {
1625    match state {
1626        ConnectionState::Authenticated {
1627            session_keyset,
1628            pending_key_challenges,
1629            ..
1630        } => {
1631            let challenge = pending_key_challenges.remove(&pubkey).ok_or_else(|| {
1632                crate::Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
1633                    reason: format!(
1634                        "no outstanding SessionKeyChallenge for pubkey '{pubkey}'; \
1635                         issue the challenge before registering"
1636                    ),
1637                }))
1638            })?;
1639            verify_challenge_response(&challenge, signature, &pubkey)
1640                .map_err(|e| crate::Error::Auth(Box::new(e)))?;
1641            session_keyset.insert(pubkey);
1642            Ok(ServiceResponse::Ok)
1643        }
1644        _ => Err(crate::Error::Auth(Box::new(
1645            AuthError::InvalidAuthConfiguration {
1646                reason: "SessionKeyRegister requires an authenticated connection; \
1647                     complete TrustedLogin* first"
1648                    .to_string(),
1649            },
1650        ))),
1651    }
1652}
1653
1654/// Resolve `pubkey`'s permission against `tree_id`'s `auth_settings` and reject
1655/// the request if the resolved level doesn't cover `required`.
1656///
1657/// If the database doesn't exist on this daemon yet and `require_existing`
1658/// is false, the gate passes through so the dispatched op surfaces its own
1659/// response (NotFound, empty result, or — for write coordination — a
1660/// no-op). This is what keeps the legitimate "create a new database" flow
1661/// working: `Database::create` reads tips on the tree before its root entry
1662/// has propagated, so an outright denial here would break creation. The
1663/// cost is that callers can still distinguish "no such database" from
1664/// "exists but no access"; closing that existence-leak channel is filed as
1665/// a follow-up.
1666///
1667/// `require_existing = true` flips that to **fail closed**: an absent
1668/// target is denied rather than waved through. Used for the
1669/// `SetInstanceMetadata` admin gate (D8) — `_databases` always exists on an
1670/// initialized daemon and that op is never a creation flow, so the
1671/// create-flow passthrough there would only ever be a fail-open hole that
1672/// lets any authenticated user rewrite the daemon's system-DB pointers if
1673/// `_databases` were ever unreadable.
1674///
1675/// When the gate does fire, the denial error is the same shape regardless
1676/// of which sub-check failed (key not in auth_settings, mismatched hint,
1677/// insufficient permission level): no internal detail about *why* leaks
1678/// back over the wire.
1679///
1680/// System databases (`_users`, `_databases`, `_sync`, `_instance`) are gated
1681/// like any other tree: callers must hold the required permission in the
1682/// system DB's `auth_settings`. The instance-admin bootstrap (first user on
1683/// the device) writes the first user as `Admin(0)` on `_users` and
1684/// `_databases`, which is how legitimate administrative access is granted —
1685/// the previous hardcoded read exemption is gone. The daemon's device-keyed
1686/// local path still handles internal system-database maintenance writes that
1687/// originate inside the server.
1688async fn gate_tree_permission(
1689    instance: &Instance,
1690    pubkey: &PublicKey,
1691    identity: &SigKey,
1692    tree_id: &ID,
1693    required: Permission,
1694    require_existing: bool,
1695) -> crate::Result<()> {
1696    let denied = || {
1697        crate::Error::Auth(Box::new(AuthError::PermissionDenied {
1698            reason: format!("tree {tree_id}: pubkey {pubkey} not permitted for {required:?}"),
1699        }))
1700    };
1701
1702    if !instance.has_database(tree_id).await {
1703        return if require_existing {
1704            Err(denied())
1705        } else {
1706            Ok(())
1707        };
1708    }
1709
1710    let database = Database::open(instance, tree_id).await?;
1711    let settings_store = database.get_settings().await?;
1712    let auth_settings = settings_store.auth_snapshot().await?;
1713
1714    let resolved =
1715        match resolve_identity_permission(pubkey, identity, &auth_settings, Some(instance)).await {
1716            Ok(p) => p,
1717            // Resolution failures (key not found, mismatch, etc.) collapse to the
1718            // same shape as an insufficient-permission denial, so the client
1719            // can't tell whether its identity was unknown or merely too low.
1720            Err(_) => return Err(denied()),
1721        };
1722
1723    let allowed = match required {
1724        Permission::Read => true,
1725        Permission::Write(_) => resolved.can_write(),
1726        Permission::Admin(_) => resolved.can_admin(),
1727    };
1728
1729    if !allowed {
1730        return Err(denied());
1731    }
1732
1733    Ok(())
1734}
1735
1736/// Per-tree read gate for ops keyed by a raw entry id, which therefore
1737/// carry no inline tree id and never hit the pre-dispatch `tree_id()` gate
1738/// (`Get`). The tree to authorise against is only knowable *after* the
1739/// fetch: it is the entry's claimed `tree.root`, or — for a tree-root
1740/// entry, whose `root()` is `None` — the entry's own id.
1741///
1742/// Model B (hard multi-tenant boundary): system DBs are unencrypted and
1743/// protected solely by this gate, so a raw cross-tree `Get` MUST resolve
1744/// and check the real owning tree before returning content. Delegates to
1745/// `gate_tree_permission`, so the `has_database`-absent passthrough and the
1746/// opaque denial shape are identical to the inline-tree-id path.
1747async fn gate_entry_read(
1748    instance: &Instance,
1749    pubkey: &PublicKey,
1750    identity: &SigKey,
1751    entry: &crate::entry::Entry,
1752) -> crate::Result<()> {
1753    let owning_tree = entry.root().unwrap_or_else(|| entry.id());
1754    // create-flow passthrough (false): a Get against a tree not yet
1755    // registered on this daemon must be waved through, same as the
1756    // inline-tree-id path.
1757    gate_tree_permission(
1758        instance,
1759        pubkey,
1760        identity,
1761        &owning_tree,
1762        Permission::Read,
1763        false,
1764    )
1765    .await
1766}
1767
1768#[cfg(test)]
1769mod tests {
1770    use super::*;
1771    use crate::backend::database::InMemory;
1772    use crate::service::protocol::{Handshake, write_frame};
1773
1774    fn private_tempdir() -> tempfile::TempDir {
1775        let dir = tempfile::tempdir().unwrap();
1776        std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(PARENT_MODE)).unwrap();
1777        dir
1778    }
1779
1780    /// Helper: start a server on a temp socket, return path + shutdown sender.
1781    async fn start_test_server() -> (
1782        PathBuf,
1783        watch::Sender<()>,
1784        tokio::task::JoinHandle<crate::Result<()>>,
1785        Instance,
1786    ) {
1787        let dir = private_tempdir();
1788        let socket_path = dir.keep().join("test.sock");
1789        let (instance, _admin) = Instance::create_backend(
1790            Box::new(InMemory::new()),
1791            crate::NewUser::passwordless("admin"),
1792        )
1793        .await
1794        .unwrap();
1795        let (tx, rx) = watch::channel(());
1796        let server = ServiceServer::bind(instance.clone(), socket_path.clone())
1797            .await
1798            .unwrap();
1799        let task = tokio::spawn(server.run(rx));
1800        (socket_path, tx, task, instance)
1801    }
1802
1803    #[tokio::test]
1804    async fn test_server_starts_and_shuts_down() {
1805        let (socket_path, tx, task, _instance) = start_test_server().await;
1806        assert!(socket_path.exists());
1807        drop(tx);
1808        task.await.unwrap().unwrap();
1809        assert!(!socket_path.exists());
1810    }
1811
1812    #[tokio::test]
1813    async fn test_bind_creates_private_nested_parents_and_group_socket() {
1814        let dir = private_tempdir();
1815        let parents = [dir.path().join("one"), dir.path().join("one/two")];
1816        let socket_path = parents[1].join("test.sock");
1817        let (instance, _admin) = Instance::create_backend(
1818            Box::new(InMemory::new()),
1819            crate::NewUser::passwordless("admin"),
1820        )
1821        .await
1822        .unwrap();
1823
1824        let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
1825
1826        for parent in parents {
1827            assert_eq!(
1828                std::fs::metadata(parent).unwrap().permissions().mode() & 0o777,
1829                PARENT_MODE
1830            );
1831        }
1832        assert_eq!(
1833            std::fs::symlink_metadata(&socket_path)
1834                .unwrap()
1835                .permissions()
1836                .mode()
1837                & 0o777,
1838            SOCKET_MODE
1839        );
1840        drop(server);
1841        assert!(!socket_path.exists());
1842    }
1843
1844    #[tokio::test]
1845    async fn test_existing_group_traversable_parent_is_preserved() {
1846        let dir = private_tempdir();
1847        let parent = dir.path().join("shared");
1848        tokio::fs::create_dir(&parent).await.unwrap();
1849        tokio::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o750))
1850            .await
1851            .unwrap();
1852        let socket_path = parent.join("test.sock");
1853        let (instance, _admin) = Instance::create_backend(
1854            Box::new(InMemory::new()),
1855            crate::NewUser::passwordless("admin"),
1856        )
1857        .await
1858        .unwrap();
1859
1860        let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
1861        let parent_metadata = std::fs::metadata(&parent).unwrap();
1862        let socket_metadata = std::fs::symlink_metadata(&socket_path).unwrap();
1863
1864        assert_eq!(parent_metadata.permissions().mode() & 0o777, 0o750);
1865        assert_eq!(socket_metadata.permissions().mode() & 0o777, 0o660);
1866        assert_eq!(socket_metadata.gid(), parent_metadata.gid());
1867        let lock_metadata = std::fs::symlink_metadata(lock_path(&socket_path)).unwrap();
1868        assert_eq!(
1869            lock_metadata.permissions().mode() & 0o777,
1870            0o600,
1871            "endpoint lock must remain owner-only"
1872        );
1873        drop(server);
1874    }
1875
1876    #[tokio::test]
1877    async fn test_existing_endpoint_lock_is_restricted() {
1878        let dir = private_tempdir();
1879        let socket_path = dir.path().join("test.sock");
1880        let lock_path = lock_path(&socket_path);
1881        tokio::fs::write(&lock_path, b"").await.unwrap();
1882        tokio::fs::set_permissions(&lock_path, std::fs::Permissions::from_mode(0o660))
1883            .await
1884            .unwrap();
1885        let (instance, _admin) = Instance::create_backend(
1886            Box::new(InMemory::new()),
1887            crate::NewUser::passwordless("admin"),
1888        )
1889        .await
1890        .unwrap();
1891
1892        let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
1893        assert_eq!(
1894            std::fs::symlink_metadata(lock_path)
1895                .unwrap()
1896                .permissions()
1897                .mode()
1898                & 0o777,
1899            0o600,
1900            "existing endpoint lock must be restricted"
1901        );
1902        drop(server);
1903    }
1904
1905    #[tokio::test]
1906    async fn test_group_or_other_writable_parent_is_refused() {
1907        let dir = private_tempdir();
1908        for mode in [0o720, 0o702] {
1909            let parent = dir.path().join(format!("unsafe-{mode:o}"));
1910            tokio::fs::create_dir(&parent).await.unwrap();
1911            tokio::fs::set_permissions(&parent, std::fs::Permissions::from_mode(mode))
1912                .await
1913                .unwrap();
1914            let socket_path = parent.join("test.sock");
1915            let (instance, _admin) = Instance::create_backend(
1916                Box::new(InMemory::new()),
1917                crate::NewUser::passwordless("admin"),
1918            )
1919            .await
1920            .unwrap();
1921
1922            assert!(
1923                ServiceServer::bind(instance, &socket_path).await.is_err(),
1924                "mode {mode:o} must be refused"
1925            );
1926            assert!(!socket_path.exists());
1927        }
1928    }
1929
1930    #[tokio::test]
1931    async fn test_symlinked_parent_component_is_refused() {
1932        let dir = private_tempdir();
1933        let target = dir.path().join("target");
1934        tokio::fs::create_dir(&target).await.unwrap();
1935        let link = dir.path().join("link");
1936        std::os::unix::fs::symlink(&target, &link).unwrap();
1937        let socket_path = link.join("nested/test.sock");
1938        let (instance, _admin) = Instance::create_backend(
1939            Box::new(InMemory::new()),
1940            crate::NewUser::passwordless("admin"),
1941        )
1942        .await
1943        .unwrap();
1944
1945        assert!(ServiceServer::bind(instance, &socket_path).await.is_err());
1946        assert!(!target.join("nested").exists());
1947    }
1948
1949    #[test]
1950    fn test_permissive_umask_does_not_widen_created_parents() {
1951        let output = std::process::Command::new(std::env::current_exe().unwrap())
1952            .args([
1953                "--exact",
1954                "service::server::tests::permissive_umask_bind_helper",
1955                "--nocapture",
1956            ])
1957            .env("EIDETICA_TEST_PERMISSIVE_UMASK", "1")
1958            .output()
1959            .unwrap();
1960        assert!(
1961            output.status.success(),
1962            "child failed:\nstdout:\n{}\nstderr:\n{}",
1963            String::from_utf8_lossy(&output.stdout),
1964            String::from_utf8_lossy(&output.stderr)
1965        );
1966    }
1967
1968    #[tokio::test]
1969    async fn permissive_umask_bind_helper() {
1970        if std::env::var_os("EIDETICA_TEST_PERMISSIVE_UMASK").is_none() {
1971            return;
1972        }
1973
1974        unsafe { libc::umask(0) };
1975        let dir = private_tempdir();
1976        let parents = [dir.path().join("one"), dir.path().join("one/two")];
1977        let socket_path = parents[1].join("test.sock");
1978        let (instance, _admin) = Instance::create_backend(
1979            Box::new(InMemory::new()),
1980            crate::NewUser::passwordless("admin"),
1981        )
1982        .await
1983        .unwrap();
1984
1985        let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
1986        for parent in parents {
1987            assert_eq!(
1988                std::fs::metadata(parent).unwrap().permissions().mode() & 0o777,
1989                PARENT_MODE
1990            );
1991        }
1992        drop(server);
1993    }
1994
1995    #[tokio::test]
1996    async fn test_wrong_protocol_version() {
1997        let (socket_path, _tx, _task, _instance) = start_test_server().await;
1998
1999        let stream = tokio::net::UnixStream::connect(&socket_path).await.unwrap();
2000        let (mut reader, mut writer) = tokio::io::split(stream);
2001
2002        // Send a version that remains wrong when the protocol is bumped.
2003        let handshake = Handshake {
2004            protocol_version: PROTOCOL_VERSION.saturating_add(1),
2005        };
2006        write_frame(&mut writer, &handshake).await.unwrap();
2007
2008        // Read ack (server sends its version back)
2009        let ack: Option<HandshakeAck> = read_frame(&mut reader).await.unwrap();
2010        let ack = ack.unwrap();
2011        assert_eq!(ack.protocol_version, PROTOCOL_VERSION);
2012
2013        // Connection should be closed by server after version mismatch
2014        // Next read should get EOF
2015        let result: crate::Result<Option<ServiceResponse>> = read_frame(&mut reader).await;
2016        assert!(result.unwrap().is_none());
2017    }
2018
2019    /// Load-bearing invariant: `ConnectionState` must never hold plaintext
2020    /// signing material in any variant. The daemon participates in storage
2021    /// and challenge-response only; the rejected Branch A design held
2022    /// decrypted user keys server-side and that boundary was reinstated by
2023    /// design (see Service Architecture doc § Decision record).
2024    ///
2025    /// Structure of the test: construct each variant, destructure with named
2026    /// fields (so adding a field forces this test to be edited), and check
2027    /// the static type of each field is not `PrivateKey`. A future refactor
2028    /// that adds e.g. `decrypted_root_key: PrivateKey` to `Authenticated`
2029    /// would either fail the type check at runtime or fail the destructure
2030    /// match exhaustiveness at compile time.
2031    #[test]
2032    fn connection_state_never_holds_private_key() {
2033        use crate::auth::crypto::{PrivateKey, generate_keypair};
2034        use std::any::TypeId;
2035
2036        fn assert_not_private_key<T: 'static>(_value: &T, label: &str) {
2037            assert_ne!(
2038                TypeId::of::<T>(),
2039                TypeId::of::<PrivateKey>(),
2040                "ConnectionState field `{label}` is PrivateKey — daemon must not hold plaintext keys"
2041            );
2042        }
2043
2044        let (_signing, pubkey) = generate_keypair();
2045        let states = [
2046            ConnectionState::PreAuth,
2047            ConnectionState::AwaitingProof {
2048                username: "u".to_string(),
2049                user_uuid: "uu".to_string(),
2050                challenge: vec![1, 2, 3],
2051                expected_pubkey: pubkey.clone(),
2052            },
2053            ConnectionState::Authenticated {
2054                username: "u".to_string(),
2055                user_uuid: "uu".to_string(),
2056                login_pubkey: pubkey.clone(),
2057                session_keyset: {
2058                    let mut s = HashSet::new();
2059                    s.insert(pubkey);
2060                    s
2061                },
2062                pending_key_challenges: HashMap::new(),
2063            },
2064        ];
2065
2066        for state in &states {
2067            match state {
2068                ConnectionState::PreAuth => {}
2069                ConnectionState::AwaitingProof {
2070                    username,
2071                    user_uuid,
2072                    challenge,
2073                    expected_pubkey,
2074                } => {
2075                    assert_not_private_key(username, "AwaitingProof::username");
2076                    assert_not_private_key(user_uuid, "AwaitingProof::user_uuid");
2077                    assert_not_private_key(challenge, "AwaitingProof::challenge");
2078                    assert_not_private_key(expected_pubkey, "AwaitingProof::expected_pubkey");
2079                }
2080                ConnectionState::Authenticated {
2081                    username,
2082                    user_uuid,
2083                    login_pubkey,
2084                    session_keyset,
2085                    pending_key_challenges,
2086                } => {
2087                    assert_not_private_key(username, "Authenticated::username");
2088                    assert_not_private_key(user_uuid, "Authenticated::user_uuid");
2089                    assert_not_private_key(login_pubkey, "Authenticated::login_pubkey");
2090                    for k in session_keyset {
2091                        assert_not_private_key(k, "Authenticated::session_keyset entry");
2092                    }
2093                    for (k, ch) in pending_key_challenges {
2094                        assert_not_private_key(k, "Authenticated::pending_key_challenges key");
2095                        assert_not_private_key(
2096                            ch,
2097                            "Authenticated::pending_key_challenges challenge",
2098                        );
2099                    }
2100                }
2101            }
2102        }
2103    }
2104
2105    #[tokio::test]
2106    async fn test_authenticated_request_rejected_without_login() {
2107        // Companion to the integration test `test_unauthenticated_backend_op_rejected`
2108        // — exercises the same gate path against the raw protocol so a
2109        // regression here surfaces immediately, not just at the
2110        // `RemoteConnection` layer.
2111        let (socket_path, _tx, _task, _instance) = start_test_server().await;
2112
2113        let stream = tokio::net::UnixStream::connect(&socket_path).await.unwrap();
2114        let (mut reader, mut writer) = tokio::io::split(stream);
2115
2116        write_frame(
2117            &mut writer,
2118            &Handshake {
2119                protocol_version: PROTOCOL_VERSION,
2120            },
2121        )
2122        .await
2123        .unwrap();
2124        let _ack: Option<HandshakeAck> = read_frame(&mut reader).await.unwrap();
2125
2126        // Send an AuthenticatedDb request without completing TrustedLogin.
2127        write_frame(
2128            &mut writer,
2129            &ServiceRequest::AuthenticatedDb(Box::new(AuthenticatedDbRequest {
2130                root_id: crate::entry::ID::default(),
2131                identity: crate::auth::types::SigKey::default(),
2132                op: DatabaseOp::GetEntry {
2133                    id: crate::entry::ID::from_bytes("nonexistent"),
2134                },
2135            })),
2136        )
2137        .await
2138        .unwrap();
2139
2140        let frame: Option<ServerFrame> = read_frame(&mut reader).await.unwrap();
2141        let resp = match frame.unwrap() {
2142            ServerFrame::Response(r) => *r,
2143            other => panic!("Expected Response frame, got {other:?}"),
2144        };
2145        match resp {
2146            ServiceResponse::Error(e) => {
2147                assert_eq!(
2148                    e.module, "auth",
2149                    "expected an auth-module error from the gate; got {e:?}"
2150                );
2151            }
2152            other => panic!("Expected gate Error, got {other:?}"),
2153        }
2154    }
2155
2156    #[tokio::test]
2157    async fn test_get_instance_metadata() {
2158        let (socket_path, _tx, _task, _instance) = start_test_server().await;
2159
2160        let stream = tokio::net::UnixStream::connect(&socket_path).await.unwrap();
2161        let (mut reader, mut writer) = tokio::io::split(stream);
2162
2163        // Handshake
2164        write_frame(
2165            &mut writer,
2166            &Handshake {
2167                protocol_version: PROTOCOL_VERSION,
2168            },
2169        )
2170        .await
2171        .unwrap();
2172        let _ack: Option<HandshakeAck> = read_frame(&mut reader).await.unwrap();
2173
2174        // Request metadata
2175        write_frame(&mut writer, &ServiceRequest::GetInstanceMetadata)
2176            .await
2177            .unwrap();
2178
2179        let frame: Option<ServerFrame> = read_frame(&mut reader).await.unwrap();
2180        let resp = match frame.unwrap() {
2181            ServerFrame::Response(r) => *r,
2182            other => panic!("Expected Response frame, got {other:?}"),
2183        };
2184        match resp {
2185            ServiceResponse::InstanceMetadata(Some(_meta)) => {
2186                // Server was initialized so metadata should exist
2187            }
2188            other => panic!("Expected InstanceMetadata(Some), got {other:?}"),
2189        }
2190    }
2191
2192    #[tokio::test]
2193    async fn test_stale_socket_cleanup() {
2194        let dir = private_tempdir();
2195        let socket_path = dir.path().join("test.sock");
2196        drop(UnixListener::bind(&socket_path).unwrap());
2197        let (instance, _admin) = Instance::create_backend(
2198            Box::new(InMemory::new()),
2199            crate::NewUser::passwordless("admin"),
2200        )
2201        .await
2202        .unwrap();
2203
2204        let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
2205        assert!(tokio::net::UnixStream::connect(&socket_path).await.is_ok());
2206        drop(server);
2207        assert!(!socket_path.exists());
2208    }
2209
2210    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2211    async fn test_simultaneous_bind_refuses_one_and_winner_serves_protocol() {
2212        let dir = private_tempdir();
2213        let socket_path = dir.path().join("test.sock");
2214        let (instance, _admin) = Instance::create_backend(
2215            Box::new(InMemory::new()),
2216            crate::NewUser::passwordless("admin"),
2217        )
2218        .await
2219        .unwrap();
2220        let expected_id = instance.id();
2221        let barrier = Arc::new(tokio::sync::Barrier::new(3));
2222
2223        let contender = |instance: Instance| {
2224            let barrier = barrier.clone();
2225            let socket_path = socket_path.clone();
2226            tokio::spawn(async move {
2227                barrier.wait().await;
2228                ServiceServer::bind(instance, socket_path).await
2229            })
2230        };
2231        let first = contender(instance.clone());
2232        let second = contender(instance);
2233        barrier.wait().await;
2234        let (first, second) = tokio::time::timeout(Duration::from_secs(1), async {
2235            (first.await.unwrap(), second.await.unwrap())
2236        })
2237        .await
2238        .expect("simultaneous binds must finish within a bound");
2239
2240        let (winner, error) = match (first, second) {
2241            (Ok(winner), Err(error)) | (Err(error), Ok(winner)) => (winner, error),
2242            (Ok(_), Ok(_)) => panic!("only one simultaneous bind may succeed"),
2243            (Err(first), Err(second)) => {
2244                panic!("one simultaneous bind must succeed; got {first:?} and {second:?}")
2245            }
2246        };
2247        assert!(
2248            matches!(&error, crate::Error::Io(error) if error.kind() == std::io::ErrorKind::AddrInUse),
2249            "expected AddrInUse, got {error:?}"
2250        );
2251
2252        let (shutdown, rx) = watch::channel(());
2253        let task = tokio::spawn(winner.run(rx));
2254        let client = tokio::time::timeout(
2255            Duration::from_secs(1),
2256            Instance::connect(format!("unix://{}", socket_path.display())),
2257        )
2258        .await
2259        .expect("the winning server must answer within a bound")
2260        .expect("the winning server must complete a protocol request");
2261        assert_eq!(client.id(), expected_id);
2262
2263        drop(client);
2264        drop(shutdown);
2265        task.await.unwrap().unwrap();
2266        assert!(!socket_path.exists());
2267    }
2268
2269    #[tokio::test]
2270    async fn test_existing_regular_file_is_refused_and_preserved() {
2271        let dir = private_tempdir();
2272        let socket_path = dir.path().join("test.sock");
2273        tokio::fs::write(&socket_path, b"keep me").await.unwrap();
2274        let (instance, _admin) = Instance::create_backend(
2275            Box::new(InMemory::new()),
2276            crate::NewUser::passwordless("admin"),
2277        )
2278        .await
2279        .unwrap();
2280
2281        assert!(ServiceServer::bind(instance, &socket_path).await.is_err());
2282        assert_eq!(tokio::fs::read(&socket_path).await.unwrap(), b"keep me");
2283    }
2284
2285    #[tokio::test]
2286    async fn test_dropping_bound_server_cleans_owned_socket_only() {
2287        let dir = private_tempdir();
2288        let socket_path = dir.path().join("test.sock");
2289        let (instance, _admin) = Instance::create_backend(
2290            Box::new(InMemory::new()),
2291            crate::NewUser::passwordless("admin"),
2292        )
2293        .await
2294        .unwrap();
2295
2296        let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
2297        tokio::fs::remove_file(&socket_path).await.unwrap();
2298        tokio::fs::write(&socket_path, b"replacement")
2299            .await
2300            .unwrap();
2301        drop(server);
2302        assert_eq!(tokio::fs::read(&socket_path).await.unwrap(), b"replacement");
2303    }
2304
2305    #[tokio::test]
2306    async fn test_bind_failure_is_returned_before_server_is_ready() {
2307        let dir = private_tempdir();
2308        let parent = dir.path().join("not-a-directory");
2309        let socket_path = parent.join("test.sock");
2310        tokio::fs::write(&parent, "file").await.unwrap();
2311        let (instance, _admin) = Instance::create_backend(
2312            Box::new(InMemory::new()),
2313            crate::NewUser::passwordless("admin"),
2314        )
2315        .await
2316        .unwrap();
2317
2318        assert!(ServiceServer::bind(instance, &socket_path).await.is_err());
2319        assert!(!socket_path.exists());
2320    }
2321
2322    /// D8 regression: `require_existing` flips the create-flow passthrough
2323    /// to fail-closed. An absent target is waved through with `false` (the
2324    /// `Database::create` path) but denied with `true` (the
2325    /// `SetInstanceMetadata` admin gate), so an unreadable `_databases`
2326    /// can't become a fail-open hole.
2327    #[tokio::test]
2328    async fn test_gate_require_existing_fails_closed_on_absent_db() {
2329        use crate::auth::crypto::generate_keypair;
2330
2331        let (instance, _admin) = Instance::create_backend(
2332            Box::new(InMemory::new()),
2333            crate::NewUser::passwordless("admin"),
2334        )
2335        .await
2336        .unwrap();
2337        let (_sk, pubkey) = generate_keypair();
2338        let absent = ID::from_bytes("no-such-tree");
2339
2340        gate_tree_permission(
2341            &instance,
2342            &pubkey,
2343            &SigKey::default(),
2344            &absent,
2345            Permission::Admin(0),
2346            false,
2347        )
2348        .await
2349        .expect("create-flow passthrough must wave an absent tree through");
2350
2351        let err = gate_tree_permission(
2352            &instance,
2353            &pubkey,
2354            &SigKey::default(),
2355            &absent,
2356            Permission::Admin(0),
2357            true,
2358        )
2359        .await
2360        .expect_err("require_existing must deny an absent tree");
2361        assert!(
2362            matches!(&err, crate::Error::Auth(b) if matches!(**b, AuthError::PermissionDenied { .. })),
2363            "expected PermissionDenied, got: {err:?}",
2364        );
2365    }
2366}