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

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**: missing socket directories are created with
36//!   mode 0700. An existing parent must be owned by the daemon user, must not
37//!   be writable by group or others, and must have no symlinked path component.
38//!   The socket is mode 0660; the parent's group and traversal bits may grant
39//!   trusted group members the full service API.
40//!
41//! See the brain note "Service Architecture" § Security Model for the design rationale,
42//! including why daemon-side signing (the earlier draft) was rejected and the
43//! deferred work that grew out of that decision (hardware-backed `PrivateKey::Remote`,
44//! async `sign()`, OS-keyring caching of derived encryption keys).
45//!
46//! ## Write Coordination
47//!
48//! Client writes travel as `DatabaseOp::SubmitSignedEntry` — the daemon stores
49//! the entry `Unverified`, then runs its own verification pass before the
50//! entry is exposed on any default read.
51//!
52//! On a connected setup the daemon is also the **sole publisher** of write
53//! events for [`Database::on_write`](crate::Database::on_write) callbacks:
54//! a connected client's `Instance::put_entry` deliberately *does not* fire
55//! its local callback registry, because the daemon will round-trip a
56//! `Notification::DatabaseWrite` (carried in a `ServerFrame::Notification`
57//! envelope) back to every subscribed connection. A client subscribes to
58//! a tree lazily on the first `Database::on_write` registration via
59//! `DatabaseOp::SubscribeWrites`. Subscriptions live for the connection's
60//! lifetime; disconnecting implicitly unsubscribes everything. Because the
61//! daemon is the sole publisher and fires each tree's subscriptions with a
62//! synchronous channel send held under that tree's write lock, every
63//! subscriber — including the originating client — observes callbacks in
64//! the daemon's canonical order *per tree*, with full `previous_tips` (no
65//! client-side placeholder). See [`Database::on_write`](crate::Database::on_write)
66//! for the full ordering contract.
67//!
68//! ## V1 Limitations
69//!
70//! - **`enable_sync()` on remote Instance**: A silent no-op (returns `Ok(())`)
71//!   rather than building a client-side sync module that would race the
72//!   daemon's own sync. The daemon runs its persisted sync lifecycle, but
73//!   clients cannot administer transports or peers over the current wire
74//!   surface.
75
76pub mod client;
77pub mod error;
78pub mod protocol;
79pub mod server;
80
81pub use client::RemoteConnection;
82pub use server::ServiceServer;
83
84use std::path::PathBuf;
85
86/// Default socket path for the Eidetica service.
87///
88/// Resolution order:
89/// 1. `EIDETICA_SOCKET` environment variable, if set.
90/// 2. `$XDG_RUNTIME_DIR/eidetica/service.sock`, if `XDG_RUNTIME_DIR` is set
91///    (the standard Linux convention).
92/// 3. `/tmp/eidetica-$USER/service.sock` as a last-resort fallback.
93///
94/// Used by the daemon CLI to choose where to bind and by
95/// [`default_socket_url`] to construct the equivalent `unix://` URL for
96/// `Instance::connect`.
97pub fn default_socket_path() -> PathBuf {
98    if let Ok(socket) = std::env::var("EIDETICA_SOCKET") {
99        return PathBuf::from(socket);
100    }
101    if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
102        PathBuf::from(runtime_dir)
103            .join("eidetica")
104            .join("service.sock")
105    } else {
106        let user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
107        PathBuf::from(format!("/tmp/eidetica-{user}")).join("service.sock")
108    }
109}
110
111/// Default `unix://` URL for `Instance::connect`, derived from
112/// [`default_socket_path`].
113///
114/// Convenience for apps that want to connect to the local daemon's socket
115/// without writing the env / `$XDG_RUNTIME_DIR` resolution themselves:
116///
117/// ```ignore
118/// let instance = Instance::connect(eidetica::service::default_socket_url()).await?;
119/// ```
120pub fn default_socket_url() -> String {
121    format!("unix://{}", default_socket_path().display())
122}