elastik_core/lib.rs
1//! # Elastik — Audi-ted L5 Storage Engine
2//!
3//! `elastik-core` is a protocol-neutral storage engine: canonical paths, opaque
4//! bytes, content-addressed versioning, an HMAC-chained audit log, and a
5//! four-tier access model. **SQLite for files.**
6//!
7//! ## Quick start
8//!
9//! ```no_run
10//! # #[cfg(feature = "unstable-engine")]
11//! # async fn run() {
12//! use elastik_core::{
13//! AccessTier, Engine, Preconditions, Representation, SecretBytes, ValidatedWorldPath,
14//! };
15//! use bytes::Bytes;
16//!
17//! let engine = Engine::builder()
18//! .data_root("./data")
19//! .key(SecretBytes::new(b"shared-hmac-secret".to_vec()).expect("hmac key"))
20//! .build()
21//! .expect("engine builds");
22//!
23//! let world = ValidatedWorldPath::new("home/hello").expect("canonical path");
24//!
25//! // Store bytes at a path.
26//! engine
27//! .replace(
28//! &world,
29//! Representation::new(Bytes::from_static(b"hi"), "text/plain", Vec::new()),
30//! Preconditions::none(),
31//! AccessTier::Write,
32//! )
33//! .await
34//! .expect("write succeeds");
35//!
36//! // Retrieve bytes by path.
37//! let read = engine.read(&world, AccessTier::Read).expect("read succeeds");
38//! assert!(read.is_some());
39//! # }
40//! ```
41//!
42//! ## What the library does
43//!
44//! - **Bytes at paths.** Canonical `home/`, `tmp/`, `dev/`, `sys/`, `etc/`,
45//! `lib/`, `boot/`, `usr/`, `var/` namespaces decide durable-vs-transient
46//! without per-call configuration.
47//! - **Versions everything.** Every successful write returns an ETag; reads,
48//! replaces, and appends honour `Preconditions::if_match` / `if_none_match`.
49//! - **Audits everything.** HMAC-chained ledger; `Engine::verify_audit`
50//! returns a typed [`AuditVerify`] result and refuses to start when an
51//! existing chain is corrupted.
52//! - **Authenticates everything.** [`AccessTier`] (Anon / Read / Write /
53//! Approve) plus token-bytes verification via [`Engine::verify_token`].
54//! - **Subscribes to changes.** [`Engine::subscribe`] returns an
55//! [`EngineSubscription`] with replay-then-live ordering.
56//!
57//! ## What the library does *not* do
58//!
59//! No protocol adapters and no server runtime. Those live in the `elastik-bin`
60//! package's `elastik-core` binary and consume this library through the
61//! unstable public [`Engine`] API. In a minimal library-only build, the library
62//! does not read environment variables, does not bind sockets, and does not
63//! depend on protocol-adapter transport crates.
64//!
65//! ## Feature flags
66//!
67//! - `bundled-sqlite` *(default)* — link a bundled SQLite via `rusqlite/bundled`.
68//! - `unstable-engine` — expose the public [`Engine`] facade. **The API shape
69//! is allowed to change between minor versions while this gate stays.**
70//!
71//! Binary adapter features such as `coap`, `mqtt`, and `multi-thread` live in
72//! `bin/Cargo.toml`, not in this library package.
73//!
74//! Minimal library-only build from the repository root:
75//! `cargo build --manifest-path core/Cargo.toml --lib --no-default-features
76//! --features bundled-sqlite,unstable-engine`.
77mod audit;
78mod auth;
79mod data_lock;
80mod defaults;
81mod delete_ops;
82mod engine;
83mod engine_introspection;
84mod engine_ops;
85mod engine_trace;
86mod engine_types;
87mod etag;
88mod event;
89mod ledger;
90mod path;
91mod read_cache;
92mod state;
93mod storage_class;
94mod store;
95#[cfg(test)]
96mod test_support;
97mod world;
98mod world_ops;
99
100// Re-export protocol-neutral helpers at the crate root.
101pub(crate) use crate::state::*;
102pub(crate) use crate::storage_class::*;
103#[cfg(not(feature = "unstable-engine"))]
104pub(crate) use auth::AuthGate;
105#[cfg(feature = "unstable-engine")]
106pub use auth::{is_valid_token, AuthGate};
107pub(crate) use data_lock::acquire_data_root_writer_lock;
108#[cfg(feature = "unstable-engine")]
109pub use defaults::{
110 DEFAULT_LISTEN_REPLAY_MAX, DEFAULT_MAX_LISTEN_CONNECTIONS, DEFAULT_MAX_MEMORY_BYTES,
111 DEFAULT_MAX_WORLD_BYTES, DEFAULT_READ_CACHE_MAX_ENTRIES,
112};
113#[cfg(feature = "unstable-engine")]
114#[doc(hidden)]
115pub use engine::ShutdownToken;
116#[cfg(feature = "unstable-engine")]
117pub use engine::{Engine, EngineBuildError, EngineBuilder, EngineError};
118#[cfg(feature = "unstable-engine")]
119pub use engine_introspection::{
120 AuditBroken, AuditValid, AuditVerify, DfSnapshot, InvalidProcPath, PoolSnapshot, ProcEndpoint,
121 ValidatedProcPath, WorldUsage,
122};
123#[cfg(feature = "unstable-engine")]
124pub use engine_trace::{DeleteMetadata, EngineDeleteTraceHooks, EngineWriteTraceHooks};
125#[cfg(feature = "unstable-engine")]
126pub use engine_types::{
127 parse_etag_matchers, AccessTier, ChangeEvent, ChangeVerb, EmptyKeyError, EngineSubscription,
128 EtagMatcher, InvalidWorldPath, Preconditions, ReadResult, Representation, SecretBytes,
129 SubscribePattern, SubscriptionRecvError, ValidatedWorldPath, WriteKind, WriteResult,
130};
131#[cfg(feature = "unstable-engine")]
132pub use path::{validate_world_name, NAMESPACE_PREFIXES};
133
134// ─── helpers ────────────────────────────────────────────────────────
135
136pub(crate) fn can_write(world_name: &str, tier: auth::Tier) -> bool {
137 // Harvard gate: /lib/, /etc/, /boot/, /usr/, and audit logs
138 // require approve. /home/, /tmp/, /dev/, /sys/, and non-log
139 // /var/ worlds accept the normal token. Anon refused.
140 let needs_approve = needs_write_approve(world_name);
141 match tier {
142 auth::Tier::Anon => false,
143 auth::Tier::Read => false,
144 auth::Tier::Write => !needs_approve,
145 auth::Tier::Approve => true,
146 }
147}
148
149/// Mirrors the harvard-gate decision in `can_write`. Lifted out so
150/// `handler::execute_put` / `execute_post` can classify a rejected
151/// write as `Auth(Write)` vs `Auth(WriteApprove)` for the FSM trace
152/// without re-deriving the predicate.
153pub(crate) fn needs_write_approve(world_name: &str) -> bool {
154 exact_or_child(world_name, "lib")
155 || exact_or_child(world_name, "etc")
156 || exact_or_child(world_name, "boot")
157 || exact_or_child(world_name, "usr")
158 || exact_or_child(world_name, "var/log")
159}
160
161pub(crate) fn can_delete(tier: auth::Tier) -> bool {
162 matches!(tier, auth::Tier::Approve)
163}
164
165// (memory_write_projected_bytes / memory_append_projected_bytes were
166// removed: the snapshot-based projection they computed could be observed
167// by two concurrent writers before either had committed, letting them
168// both pass and overshoot max_memory_bytes once the global write_lock was
169// gone. Quota is now enforced inside MemoryStore::write_with_quota /
170// append_with_quota, atomically with the write itself.)
171
172// `require_read` (auth gate for proc handlers) lives in proc.rs now,
173// next to its only callers.
174
175pub(crate) fn exact_or_child(world_name: &str, prefix: &str) -> bool {
176 world_name == prefix
177 || world_name
178 .strip_prefix(prefix)
179 .is_some_and(|rest| rest.starts_with('/'))
180}
181
182pub(crate) fn can_read(core: &Core, tier: auth::Tier) -> bool {
183 !core.tokens.read_required()
184 || matches!(
185 tier,
186 auth::Tier::Read | auth::Tier::Write | auth::Tier::Approve
187 )
188}