Skip to main content

max / synckit

2.3 KB · 48 lines History Blame Raw
1 //! Local key/value app settings, with a per-key sync posture.
2 //!
3 //! The store half of SyncKit's config sync, and the whole of it for an app that
4 //! does not sync. A [`ConfigStore`] is a SQLite key/value table an app reads and
5 //! writes with no network in sight; a [`ConfigSpec`] declares which of those keys
6 //! may cross the sync boundary. `synckit-client` turns a spec into a
7 //! `SyncTable`, but nothing here depends on it, so a no-sync consumer — the Alloy
8 //! console remembering a theme — links this crate and not the sync runtime.
9 //!
10 //! ## Why this is its own crate, and why it is SQLite
11 //!
12 //! Four apps stored settings four ways (a synced SQLite table, an
13 //! unconditionally-synced one, `localStorage`, a TOML file) and the theme
14 //! extraction was about to make it five. The unifying substrate is SQLite rows,
15 //! not a text file, because config syncs **per key**: two devices editing
16 //! different keys must both survive (row-level HLC merge, which a whole-file blob
17 //! cannot do), and some keys must never sync at all (see posture). Rows are what
18 //! buy both. A text file would need a projection layer and a sidecar for the
19 //! per-key merge metadata — SQLite by the back door. Design note:
20 //! `synckit-config-design` in the wiki.
21 //!
22 //! ## Posture, and why it fails closed
23 //!
24 //! [`Posture`] is the one security-load-bearing type here. It descends from
25 //! audiofiles' `DeviceLocal`/`Replicated`, which exists because a hostile server
26 //! steered a local filesystem path across the sync boundary through an
27 //! unclassified config key (fuzz-2026-07-06 #2, -07-20 #1, -07-21 #3). Two rules
28 //! carried over intact:
29 //!
30 //! - The default is [`Posture::Local`]. A key crosses only when a spec declares
31 //! it [`Posture::Synced`].
32 //! - An **undeclared** key is `Local`, never `Synced`. So the sync filter a spec
33 //! drives fails closed: a key nobody classified stays on the device rather than
34 //! leaking because someone forgot to name it.
35
36 #![forbid(unsafe_code)]
37
38 mod spec;
39 mod store;
40
41 pub use spec::{ConfigSpec, PolicyRow, Posture};
42 pub use store::{ConfigError, ConfigStore, Result};
43
44 /// Re-exported so a consumer can open the [`Connection`](rusqlite::Connection)
45 /// that [`ConfigStore::open`] takes without depending on rusqlite itself, nor
46 /// version-matching it. A no-sync app (the Alloy console) links only this crate.
47 pub use rusqlite;
48