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}