feat: gate the store behind a feature and document the schema as a contract
This commit is contained in:
parent
8fb5661b53
commit
aa3950bc80
6 changed files with 122 additions and 16 deletions
28
README.md
28
README.md
|
|
@ -64,7 +64,7 @@ nix build # or: nix develop -c cargo build
|
|||
nix develop -c cargo test --workspace
|
||||
```
|
||||
|
||||
The crate is a workspace: `core/` is the `fumi-core` library — everything but the command line — and `cli/` is the `fumi` binary. GUI hosts depend on `fumi-core` alone; it pulls no argument parsing, no logging globals, and no keyfile conventions. `bundled-sqlite` (default) vendors libsqlite3; embedders that link their own SQLite, as Android's NDK does, build with `default-features = false`.
|
||||
The crate is a workspace: `core/` is the `fumi-core` library — everything but the command line — and `cli/` is the `fumi` binary. GUI hosts depend on `fumi-core` alone; it pulls no argument parsing, no logging globals, and no keyfile conventions. `store` (default) is the SQLite local-state module and the operations that take it; `bundled-sqlite` (default) vendors libsqlite3, so embedders that link their own SQLite, as Android's NDK does, build with `default-features = false` and that flag alone. With both defaults off the crate keeps identities, addresses, seal/open and the raw transport operations (`resolve`, `walk_chain`, `accept_mac`) — for hosts with their own database and thin FFI consumers that need only the crypto and envelope layer.
|
||||
|
||||
`bunshin` and the reference clients make a complete test rig: register against a local server, exchange mail in both directions, then rotate and fetch the mail that the superseded key still receives.
|
||||
|
||||
|
|
@ -73,7 +73,8 @@ The crate is a workspace: `core/` is the `fumi-core` library — everything but
|
|||
`fumi-core` is the protocol core of a GUI client, not just this CLI's engine. The contract an embedder can rely on:
|
||||
|
||||
- **Secrets stay bytes in the API, files stay in the CLI.** `Account::new(master, index)` takes the master as bytes and `Store::open(path)` takes only mail and state; `identity.key` next to `fumi.db` is a CLI convention. A GUI keeps the master wherever its platform wants it (Android Keystore, OS keychain) and hands it over per operation.
|
||||
- **One shareable store handle.** `Store` is `Send + Sync`: every operation locks an inner mutex, and the database runs in WAL mode, so a host holds one `Store` behind an executor and reads it while a fetch writes. Each envelope is committed as it is verified — a concurrent reader listing the inbox sees fetch progress without any callback.
|
||||
- **A storeless build.** `default-features = false` drops rusqlite entirely: identities, addresses, seal/open and the raw transport operations remain, for hosts with their own database. `store` and `bundled-sqlite` compose with it as separate features.
|
||||
- **One shareable store handle.** `Store` is `Send + Sync`: every operation locks an inner mutex, and the database runs in WAL mode, so a host holds one `Store` behind an executor and reads it while a fetch writes. Each envelope is committed as it is verified — a concurrent reader listing the inbox sees fetch progress without any callback. `Store::open_in_memory()` gives a scratch store with the schema and version already in place — for tests, foreign importers assembling rows in code, and FFI round-trips that never touch disk.
|
||||
- **Decisions, not prose.** `error::Error` is an enum: `NotPinned`, `PinMismatch`, `KeyChanged { address, known, offered }`, `NotRegistered`, `AuthFailed`, `RateLimited`, `QuotaExceeded`, `SchemaVersion`, ... — the distinctions a UI routes on. `Display` still produces the message the CLI prints.
|
||||
- **Long operations can stop.** `fetch_with` takes a `FetchOptions` with a cancellation token checked between pages; the cursor is saved per page, so a cancelled fetch resumes rather than repeats.
|
||||
- **Composable flows.** `restore` remains the one-call convenience, but its pieces are public: `connect` + `resolve`, `account::find_rotation_index`, then the `Store` setters — a wizard can hold the master and the address before it has connectivity, and show each step. Trust outcomes arrive as values: `TrustChange` from `trust_key` and `send`, `Session::unpinned_static` for the sec 4 warning.
|
||||
|
|
@ -82,6 +83,25 @@ The crate is a workspace: `core/` is the `fumi-core` library — everything but
|
|||
|
||||
The intended call graph: `Store::open` once, `Account::new(master, rotations)` per session, then `client::*` operations taking `&Store` and `&Account`. The store outlives accounts; nothing in the library caches between calls.
|
||||
|
||||
## The store schema
|
||||
|
||||
The schema is a contract, not an implementation detail: `SCHEMA_VERSION` is 1, written to SQLite's `PRAGMA user_version` at creation and checked at open, so a foreign importer — a migration from another client, a sync tool — can build rows directly instead of reading `store.rs`. `Store::open_in_memory()` gives an importer a scratch database with schema and version in place. Within a version the layout below is stable; a version bump comes with a documented migration.
|
||||
|
||||
Every mail column holds a sealed envelope, never plaintext. Timestamps are Unix seconds; keys and ids are 32-byte blobs.
|
||||
|
||||
| Table | Columns | Meaning and invariants |
|
||||
|---|---|---|
|
||||
| `state` | one row, `id = 1`, always present | `username`, `host`, `port`, `scheme` (`tcp` or `rns`) are the account; `rotations` is the sec 2 rotation index the account is at; `after_time` and `after_id` are the sec 6.1 fetch cursor; `sync_ok` (0/1) says whether the local accept-token set may replace the server's — a client restored from the master alone must not erase it (sec 4) |
|
||||
| `servers` | `host`, `static`, `pinned_at` | The sec 4 pins: a server's Noise static key, keyed by host without port |
|
||||
| `contacts` | `address`, `identity`, `verified`, `seen_at` | Every key known for an address; `verified` is 1 only when it came from a self-certifying URI |
|
||||
| `accepted` | `address`, `identity`, `active`, `added_at` | Main-tier admission (sec 5.8). `identity` is frozen at acceptance because the token the correspondent holds is derived from it; `active = 0` is a block and keeps the row |
|
||||
| `tokens` | `address`, `token`, `seen_at` | Accept tokens correspondents issued us, filed under their address, which outlives the keys behind it (sec 5.8) |
|
||||
| `seen` | `id`, `at` | Every message id ever fetched; a replay is not re-stored even after a local delete (sec 10) |
|
||||
| `inbox` | `id`, `envelope`, `received_at`, `tier` | Sealed mail; `tier` is 0 main, 1 requests (arrived without a matching accept token) |
|
||||
| `sent` | `id`, `recipient`, `envelope`, `sent_at` | Sealed self-copies (sec 5.6), with the recipient for the listing |
|
||||
|
||||
An importer should set `user_version` to 1, keep the single `state` row, and respect the frozen `accepted.identity` and `sync_ok` semantics above — those are the two rows where a wrong guess loses data (an erased server-side token set, or a contact's token silently changing).
|
||||
|
||||
## Modules
|
||||
|
||||
| File | Contents |
|
||||
|
|
@ -93,8 +113,8 @@ The intended call graph: `Store::open` once, `Account::new(master, rotations)` p
|
|||
| `core/src/transport.rs` | `Transport` trait, bind values, operation and status constants |
|
||||
| `core/src/tcp.rs` | Noise_NX initiator, `len u32 \|\| op u8 \|\| body` framing, static-key pinning |
|
||||
| `core/src/rns/` | the Reticulum carrier behind the `rns` feature: FFI over `core/shim/`, path discovery, one link per session |
|
||||
| `core/src/client.rs` | the six operations, AUTH, chain walking, token sync, fetch pipeline with cancellation |
|
||||
| `core/src/store.rs` | SQLite: state, contacts, accepted, tokens, seen ids, inbox, sent; `Send + Sync`, WAL, versioned schema |
|
||||
| `core/src/client.rs` | the six operations, AUTH, chain walking, token sync, fetch pipeline with cancellation; store-taking items behind the `store` feature |
|
||||
| `core/src/store.rs` | SQLite behind the `store` feature: state, contacts, accepted, tokens, seen ids, inbox, sent; `Send + Sync`, WAL, versioned schema |
|
||||
| `core/src/error.rs` | the `Error` enum embedders match on, the status-code mapping |
|
||||
| `core/vectors.json` | the committed reference vectors, the tests' source of truth |
|
||||
| `cli/src/main.rs` | the command line: keyfile and `--db` conventions, hints, output |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue