eidetica/service/mod.rs
1//! Local service (daemon) mode for Eidetica.
2//!
3//! This module enables running Eidetica as a local daemon that serves an Instance
4//! to multiple client processes over a Unix domain socket. The primary motivation is
5//! shared storage: multiple CLI tools and applications can operate on the same
6//! Eidetica data without each process opening its own backend.
7//!
8//! ## Architecture
9//!
10//! The RPC boundary sits at the storage operation level. A `RemoteConnection` forwards
11//! all operations over a Unix socket to the daemon, backing the `RemoteBackend` seam impl.
12//! `Instance::connect(path)` loads `InstanceMetadata` from the remote backend,
13//! then constructs an Instance with no local secrets.
14//!
15//! ## Security Model
16//!
17//! Client-side signing. The daemon stores and serves encrypted key material and
18//! signed entries but never holds plaintext user signing keys or passwords.
19//!
20//! - **User keys stay client-side**: clients fetch encrypted `UserCredentials` from
21//! the daemon, derive the key-encryption-key locally (Argon2id), decrypt the user's
22//! signing key in-process, and sign entries before sending them to the daemon for
23//! storage. The signing key never crosses the socket.
24//! - **Authentication via challenge-response**: when the daemon needs to prove a
25//! connecting client controls a user account, the daemon issues a fresh random
26//! challenge per session and the client signs it with the user's root key. The
27//! daemon verifies against the user's public key from its auth tables. No password
28//! is sent over the wire; successful decryption of the user's signing key on the
29//! client *is* password verification.
30//! - **Encrypted stores remain opaque to the daemon**: per-database encrypted CRDTs
31//! (e.g. `PasswordStore`) merge as `Vec<EncryptedBlob>` โ the daemon participates
32//! in storage and sync without ever holding a content encryption key. Clients
33//! decrypt and merge in-process and may write the result back as an encrypted
34//! cache entry.
35//! - **Filesystem permissions**: the socket directory is owner-only (mode 0700) and
36//! the socket itself is mode 0600 as an additional access-control layer.
37//!
38//! See the brain note "Service Architecture" ยง Security Model for the design rationale,
39//! including why daemon-side signing (the earlier draft) was rejected and the
40//! deferred work that grew out of that decision (hardware-backed `PrivateKey::Remote`,
41//! async `sign()`, OS-keyring caching of derived encryption keys).
42//!
43//! ## Write Coordination
44//!
45//! Client writes travel as `DatabaseOp::SubmitSignedEntry` โ the daemon stores
46//! the entry `Unverified`, then runs its own verification pass before the
47//! entry is exposed on any default read.
48//!
49//! ## V1 Limitations
50//!
51//! - **No server-push notifications**: Clients see latest state on each request
52//! but are not notified when the daemon receives entries from sync peers, or
53//! when another client writes through the same daemon. As a consequence,
54//! [`Database::on_write`](crate::Database::on_write) on a connected
55//! [`Instance`](crate::Instance) observes only writes this client's own
56//! commit path produced โ see that method's doc for the full caveat list.
57//! Future: bidirectional framing with a `Notification` variant alongside
58//! `Response`, plus a client-side reader task that routes notifications to
59//! the local callback registry.
60//!
61//! - **`enable_sync()` on remote Instance**: A silent no-op (returns `Ok(())`)
62//! rather than building a client-side sync module that would race the
63//! daemon's own sync. The daemon either already runs sync or it does not,
64//! and the client cannot change that over the current wire surface. Future:
65//! add an admin-gated `EnableSync` RPC that delegates to the server's
66//! Instance, and similarly for `sync()`, `flush_sync()`, etc.
67
68pub mod client;
69pub mod error;
70pub mod protocol;
71pub mod server;
72
73pub use client::RemoteConnection;
74pub use server::ServiceServer;
75
76use std::path::PathBuf;
77
78/// Default socket path for the Eidetica service.
79///
80/// Resolution order:
81/// 1. `EIDETICA_SOCKET` environment variable, if set.
82/// 2. `$XDG_RUNTIME_DIR/eidetica/service.sock`, if `XDG_RUNTIME_DIR` is set
83/// (the standard Linux convention).
84/// 3. `/tmp/eidetica-$USER/service.sock` as a last-resort fallback.
85///
86/// Used by the daemon CLI to choose where to bind and by
87/// [`default_socket_url`] to construct the equivalent `unix://` URL for
88/// `Instance::connect`.
89pub fn default_socket_path() -> PathBuf {
90 if let Ok(socket) = std::env::var("EIDETICA_SOCKET") {
91 return PathBuf::from(socket);
92 }
93 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
94 PathBuf::from(runtime_dir)
95 .join("eidetica")
96 .join("service.sock")
97 } else {
98 let user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
99 PathBuf::from(format!("/tmp/eidetica-{user}")).join("service.sock")
100 }
101}
102
103/// Default `unix://` URL for `Instance::connect`, derived from
104/// [`default_socket_path`].
105///
106/// Convenience for apps that want to connect to the local daemon's socket
107/// without writing the env / `$XDG_RUNTIME_DIR` resolution themselves:
108///
109/// ```ignore
110/// let instance = Instance::connect(eidetica::service::default_socket_url()).await?;
111/// ```
112pub fn default_socket_url() -> String {
113 format!("unix://{}", default_socket_path().display())
114}