on master help sign in
Rewrite CLAUDE.md to reflect actual Rust/Crux + Swift + Knot codebase

The old CLAUDE.md described a deleted F#/.NET/Avalonia client with libsodium. The real codebase is a Rust shared core (crux_core, boltffi, ed25519-dalek, x25519-dalek, chacha20poly1305, zstd), a thin SwiftUI shell (iOS 17+), and a Knot relay server. New file covers: tech stack, file-by-file layout, just recipes, MVU architecture (shell-facing vs internal events, flat ViewModel), code conventions per language, and gotchas (.cargo/config.toml is load-bearing, stale F# artifacts, PROTOCOL.md behind code on stale-profile rejection, federation unauthenticated by design).

commit591222613f41d2b27e4219b2676cffad6e7ffdd7 authoriko <mail@iko.soy> dateSat 11 Jul 2026, 07:43 (3 months ago) parent91b7b58
1 file changed+101−35
CLAUDE.md+101−35
CLAUDE.md+101−35
@@ -1,54 +1,120 @@
11# CLAUDE.md
22
3−Decentralized E2E encrypted messenger. No accounts, no home servers — identity is a keypair.
3+Decentralized, end-to-end encrypted messenger. No accounts, no home servers — identity is an Ed25519 keypair. Messages route over plain HTTP through any relay in an open federated mesh.
4+
5+`PROTOCOL.md` is the normative spec (v0.1 draft) and should be read before touching crypto, the wire format, or the server. The relay sees only the recipient pubkey and an opaque blob; the sender's identity and signature live *inside* the AEAD ciphertext, so a server never learns who sent a message.
46
57## Tech Stack
68
7−- **Client:** F# / .NET 10 / Fabulous (Elm-style MVU) + Avalonia UI
8−- **Server:** Knot (custom functional language, compiled via Rust toolchain)
9−- **Crypto:** libsodium (Ed25519 + X25519 + XChaCha20-Poly1305), LZ4 plaintext compression
10−- **Persistence:** SQLite — client via Donald + Microsoft.Data.Sqlite; server via the Knot runtime, which transparently persists every `*ref` to SQLite
9+- **Shared core:** Rust (edition 2021, `rust-version` 1.90) built on [Crux](https://redbadger.github.io/crux/) — `crux_core` (MVU), `crux_http`, `crux_kv`, `crux_time`. All business logic lives here: identity, auth, the long-poll loop, the outbox, contacts, profiles, message ingest.
10+- **FFI:** `boltffi` exports `CoreFFI` (`core/src/ffi.rs`) as a bincode bridge; `facet` drives Swift type generation from the core's `Event` / `ViewModel` / `Effect` types.
11+- **Apple shell:** Swift + SwiftUI, **iOS 17+ only** (`include_macos = false` in `core/boltffi.toml`). The shell is a thin effect executor — it performs HTTP, key-value, and timer effects and renders the `ViewModel`. It contains no business logic.
12+- **Crypto:** `ed25519-dalek` + `x25519-dalek` + `curve25519-dalek`, `chacha20poly1305` (XChaCha20-Poly1305), `hkdf` + `sha2` (HKDF-SHA256), `zstd` (plaintext compression). **No libsodium** — `core/src/crypto.rs` hand-reimplements libsodium's `crypto_sign_ed25519_{pk,sk}_to_curve25519` so blobs stay wire-compatible with any spec-following client.
13+- **Server:** `server.knot`, written in [Knot](https://github.com/ilyakooo0/knot) — a custom functional language compiled to a native binary. Its runtime transparently persists every `*ref` to SQLite, so all server state (`*sessions`, `*messages`, `*presence`, …) survives restarts despite reading like in-memory lists.
14+- **Persistence (client):** `crux_kv` — JSON blobs under the keys `settings`, `profile`, `contacts`, `cursor`, `outbox`, and `messages:<peer_hex>`. The Swift shell backs this with one file per key under Application Support. The 64-byte Ed25519 secret key lives in the iOS Keychain, never in kv.
1115
1216## Project Layout
1317
14−- `client/` — F# cross-platform app (macOS desktop, iOS)
15− - `App.fs` — Top-level state machine (Model / Msg / CmdMsg / update)
16− - `AppTypes.fs` — Shared model/msg/envelope types and helpers
17− - `Constants.fs` — App-wide constants (HKDF info, poll timing, profile/settings IDs)
18− - `Crypto.fs` — Encryption/decryption, key derivation, signing
19− - `ApiClient.fs` — HTTP transport (auth, poll, send)
20− - `Protocol.fs` — Envelope serialize/parse + send orchestration
21− - `Store.fs` — SQLite persistence (contacts, messages, settings, profile, outbox)
22− - `Phonemic.fs` — Urbit @p syllable encoding for human-readable keys
23− - `Buttons.fs`, `Labels.fs`, `TextFields.fs`, `Styles.fs`, `Keyboard.fs` — UI primitives
24− - `ViewSetup.fs`, `ViewConversations.fs`, `ViewChat.fs`, `ViewAddContact.fs`, `ViewSettings.fs`, `ViewEditProfile.fs` — Per-page views
25− - `Platform/Desktop/` — macOS entry point + `.app` bundle wiring
26− - `Platform/iOS/` — iOS entry point, native photo picker, native QR scanner
27−- `server.knot` — Complete server implementation (routes, federation, gossip, presence, retries)
28−- `servers.json` — Public relay list (flat JSON array of URLs)
29−- `install.sh` — Linux deployment (downloads release binary, sets up systemd + Caddy)
30−- `PROTOCOL.md` — Wire protocol specification
18+```
19+core/ Rust shared core (crate `skrepka_core`)
20+ src/app.rs Crux App: Event / Model / ViewModel / Effect + update(). The whole state machine.
21+ src/model.rs Persisted state types (Settings, OwnProfile, Contact, StoredMessage, OutboxItem)
22+ and the flat ViewModel the shell renders.
23+ src/crypto.rs Identity, encrypt/decrypt, ed25519→x25519 derivation, auth-challenge signing.
24+ src/protocol.rs Plaintext payload (de)serialization (text / delivery.ack / profile) + Envelope.
25+ src/phonemic.rs Urbit-style @p syllable encoding of pubkeys; try_parse_pubkey accepts @p or hex.
26+ src/ffi.rs boltffi surface: update / resolve / view over bincode bytes.
27+ src/bin/codegen.rs Emits Swift bindings via facet typegen (feature `codegen`).
28+ boltffi.toml Apple packaging config (iOS 17 target, module `SkrepkaShared`).
29+
30+apple/
31+ project.yml xcodegen spec → SkrepkaApp.xcodeproj (bundle lol.skrepka.SkrepkaApp-iOS).
32+ SkrepkaApp/SkrepkaApp.swift @main entry point.
33+ SkrepkaApp/Core.swift Drives the core: serializes events in, dispatches effects out, resolves them back.
34+ SkrepkaApp/Effects.swift Effect handlers — Http (URLSession, 70s timeout), Time, KvStore (file per key),
35+ Keychain (loads or generates the 64-byte identity key).
36+ SkrepkaApp/Views.swift RootView, ConversationsView, ChatView, MessageBubble, Avatar.
37+ SkrepkaApp/Forms.swift AddContactView, SettingsView, EditProfileView.
38+ SkrepkaApp/QRView.swift QR generation + AVFoundation camera scanner.
39+ SkrepkaApp/PhotoPicker.swift PHPicker → downscaled 256px JPEG as base64.
40+ generated/ gitignored — Shared/ (xcframework + SPM package) and App/Skrepka (Swift types).
41+
42+server.knot The entire relay: config, types, routes, auth, poll, send, federation, background loops.
43+servers.json Public relay list (flat JSON array of URLs). A static suggestion, not a directory.
44+install.sh Linux deploy: downloads the release binary, writes a systemd unit, configures Caddy + TLS.
45+skrepka.service Reference systemd unit (install.sh writes its own, with --http-max-body-bytes=42M).
46+Justfile All build/run recipes.
47+.cargo/config.toml Pins IPHONEOS/MACOSX deployment targets (see Gotchas).
48+.github/workflows/build.yml CI: compiles server.knot on linux-x86_64 / linux-arm64 / macos-arm64, cuts a release.
49+PROTOCOL.md Wire protocol specification.
50+PROTOCOL-REVIEW.md Audit of spec-vs-implementation gaps (written against the old F# client — see Gotchas).
51+```
3152
3253## Build & Run
3354
34−```bash
35−# macOS desktop
36−cd client && ./run-mac.sh
55+One-time setup:
56+
57+```sh
58+rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
59+cargo install boltffi_cli
60+brew install xcodegen
61+```
3762
38−# iOS simulator
39−cd client && ./run-ios.sh
63+Recipes (`just <name>`; `just` alone runs `test`):
4064
41−# Plain build
42−cd client && dotnet build
65+```sh
66+just test # cargo test -p skrepka_core (crypto, protocol, @p, state machine)
67+just typegen # regenerate Swift types into apple/generated/App
68+just pack # build the core xcframework + Swift package into apple/generated/Shared
69+just project # xcodegen generate → apple/SkrepkaApp.xcodeproj
70+just generate # typegen + pack + project — run this after any core type change
71+just build SIM_ID=<udid> # xcodebuild for a booted simulator
72+just run SIM_ID=<udid> # install + launch on that simulator
73+just server # wipe server.db* and run the local ./server binary
4374```
4475
45−## Version Control
76+`just server` expects a **prebuilt `./server`** binary; there is no local recipe that compiles it. To build it you need the Knot compiler (`knot build server.knot`), which CI downloads from `ilyakooo0/knot` releases — on Linux it links statically against musl. `server` and `*.db*` are gitignored.
77+
78+Deploying a relay:
79+
80+```sh
81+curl -fsSL https://raw.githubusercontent.com/ilyakooo0/skrepka/master/install.sh | sudo DOMAIN=relay.example.com sh
82+```
83+
84+Every top-level scalar constant in `server.knot` is overridable at startup as `--name=value` (e.g. `--serverHost=relay.example.com`, `--federationEnabled=False`, `--trustForwardedFor=False`); `--help` lists them all.
4685
47−Use **jj** (jujutsu), not git.
86+## Architecture
87+
88+The core is a pure MVU state machine. `Skrepka::update(Event, &mut Model) -> Command<Effect, Event>` is the only place state changes; `Skrepka::view(&Model) -> ViewModel` is the only thing the shell reads. The shell never decides anything — it turns `Effect`s into platform calls and resolves the results back in.
89+
90+Two kinds of `Event` variants, and the distinction matters:
91+
92+- **Shell-facing** (`IdentityLoaded`, `SendText`, `OpenChat`, `AddContact`, …) — cross the FFI, so they appear in the generated Swift `Event` enum.
93+- **Internal** (`LoadedContacts`, `PollResult`, `SendResult`, …) — carry non-FFI-safe payloads (`crux_http::Result`, `KeyValueError`) and are marked `#[serde(skip)] #[facet(skip)] #[facet(opaque)]` so they never reach Swift. Adding a new internal event means adding those attributes too, or typegen breaks.
94+
95+The `ViewModel` is deliberately flat and stringly-typed (`page: String`, `conn_status: String`) so the Swift side switches on plain strings rather than needing generated enums for UI state.
96+
97+Runtime flow: `IdentityLoaded` (from the Keychain, at boot) fans out five kv loads → `LoadedSettings` triggers `Connect` → `Authenticate` (challenge/verify, signature bound to the server's bare hostname under the `skrepka-auth-v1:` tag) → on a token, `Poll` (server long-polls ~25 s, client re-polls immediately) and `StartFlush` (drains the outbox one message at a time, encrypting at send time). Failures back off via `crux_time` — 3 s doubling to a 30 s cap.
4898
4999## Code Conventions
50100
51−- Formatter: Fantomas (`dotnet tool restore && dotnet fantomas client/`)
52−- F# naming: PascalCase types, camelCase functions/fields
53−- Architecture: Elm-style MVU — all state in `Model`, all effects via `CmdMsg`
54−- No mutable state outside UI framework
101+- **Rust:** standard `rustfmt`; the code is written clippy-clean (note the targeted `#[allow]`s in `app.rs` / `ffi.rs`). Doc comments (`//!`, `///`) carry the design rationale and cite `PROTOCOL.md` sections — keep that habit; it's how the crypto stays auditable against the spec.
102+- **Swift:** SwiftUI, no business logic. `Core.swift` uses `try!` on bincode round-trips deliberately (a serialization mismatch is a build-time bug, not a runtime condition) with `swiftlint:disable` comments marking each.
103+- **Knot:** `server.knot` is written for clarity over performance — every collection is a flat list, every scan is O(n). The header comment says so explicitly. Its dense comments encode security reasoning (SSRF deny-list, rate-limit key choice, replay/dedup invariants); read them before changing behavior.
104+- Anything on the wire is **lowercase hex**; anything shown to a human can be **@p** (`ridler-binzod-…`). `try_parse_pubkey` accepts both.
105+
106+## Version Control
107+
108+Convention is **jj** (jujutsu), not git — but note the working copy is currently a **plain git checkout with no `.jj/`**. Run `jj git init --colocate` before using jj commands here.
109+
110+When reading diffs, pass `--git` to `jj diff` / `jj show` — the configured formatter is difftastic, whose ANSI output is meant for terminals, not models.
111+
112+## Gotchas
113+
114+- **Regenerate after core type changes.** Touching `Event`, `ViewModel`, or `Effect` without running `just generate` leaves the Swift side compiling against stale types.
115+- **`.cargo/config.toml` is load-bearing.** It pins `IPHONEOS_DEPLOYMENT_TARGET=17.0` / `MACOSX_DEPLOYMENT_TARGET=11.0` so `zstd-sys` (a C dep) and rustc agree on the minimum OS version. Without it the linker fails on a version mismatch.
116+- **Body-size cap on the relay.** `--http-max-body-bytes` must exceed `maxBlobLen` (40 MiB hex) plus the JSON envelope, or the runtime's default 16 MiB cap rejects a max-size blob with a bare `413` before the handler's `400 invalid_message` check runs. `install.sh` sets `42M`.
117+- **Refined types don't validate inside lists.** Knot's route-boundary checks only fire on top-level scalar body fields, so `handleSendMessages` and `handleRecvGossip` re-validate nested `to` / `pubkey` / `encryptedBlob` by hand. Preserve those checks.
118+- **Stale artifacts from the deleted F# client.** `README.md` (describes an F#/Avalonia app and links a nonexistent logo), `dotnet-tools.json` (Fantomas), parts of `.gitignore` (`client/bin`, `client/obj`, `.fake`), and `PROTOCOL-REVIEW.md` (cites `client/ApiClient.fs`) all predate the Rust rewrite. Don't treat them as descriptions of the current code.
119+- **PROTOCOL.md is behind the core in one place.** §4 and §10 claim "the reference client does not yet enforce" stale-`profile` rejection. The Rust core *does* enforce it — `ingest_poll` in `app.rs` drops any `profile` whose `ts` predates `Contact::last_profile_ts`.
120+- **Federation is unauthenticated by design.** `/federation/*` endpoints accept any caller; abuse is bounded by the SSRF deny-list, the `no_presence` gate, and per-IP rate limits — not by peer authentication. `PROTOCOL.md` §10 enumerates what that does and doesn't protect.