refactor: split the library into fumi-core and make it embeddable

This commit is contained in:
randogoth 2026-09-28 23:21:08 +03:00
parent d3cd0afb4f
commit 8fb5661b53
28 changed files with 975 additions and 724 deletions

View file

@ -61,26 +61,45 @@ Mail is stored sealed and opened on demand; there is no plaintext at rest. A fir
```
nix build # or: nix develop -c cargo build
nix develop -c cargo test
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`.
`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.
## Embedding
`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.
- **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.
- **A versioned store.** The schema carries a `user_version`; a store written by a newer build is refused (`Error::SchemaVersion`) rather than misread, and within a version the schema is stable. The host, not the library, decides when to upgrade the binary.
- **Binding from other languages.** The API is `Send`-friendly and free of CLI globals, so plain Rust bindings (flutter_rust_bridge, uniffi) work against it. `core/vectors.json` holds the reference vectors the unit tests pin — derivations, seals, a full envelope — so a port or FFI binding can verify itself end to end without the reference stack.
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.
## Modules
| File | Contents |
|---|---|
| `src/crypto.rs` | base32, HKDF, HMAC, SHA-256, the Ed25519→X25519 map with SPEC.md §2's checks |
| `src/address.rs` | parsing for all four address forms, username validation, fingerprints |
| `src/account.rs` | master, `seed_n` derivation, accept-key and tokens, rotation certificates |
| `src/message.rs` | envelope seal/open (§5.1–§5.3), message id (§5.4), frontmatter (§5.5) |
| `src/transport.rs` | `Transport` trait, bind values, operation and status constants |
| `src/tcp.rs` | Noise_NX initiator, `len u32 \|\| op u8 \|\| body` framing, static-key pinning |
| `src/rns/` | the Reticulum carrier behind the `rns` feature: FFI over `shim/`, path discovery, one link per session |
| `src/client.rs` | the six operations, AUTH, chain walking, token sync, fetch pipeline |
| `src/store.rs` | SQLite: state, contacts, accepted, tokens, seen ids, inbox, sent |
| `core/src/crypto.rs` | base32, HKDF, HMAC, SHA-256, the Ed25519→X25519 map with SPEC.md §2's checks |
| `core/src/address.rs` | parsing for all four address forms, username validation, fingerprints |
| `core/src/account.rs` | master, `seed_n` derivation, accept-key and tokens, rotation certificates, `find_rotation_index` |
| `core/src/message.rs` | envelope seal/open (§5.1–§5.3), message id (§5.4), frontmatter (§5.5) |
| `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/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 |
Unit tests pin every derivation against vectors generated from `smolmail.py`, including a full envelope reproduced byte for byte.
Unit tests pin every derivation against `vectors.json`, whose values were generated from `smolmail.py`, including a full envelope reproduced byte for byte.
## References