docs: document the onboarding/settings rework and the string pipeline
This commit is contained in:
parent
70087fa470
commit
c887412432
1 changed files with 15 additions and 4 deletions
19
README.md
19
README.md
|
|
@ -18,7 +18,7 @@ browser (all crypto, all keys) ⇄ bridge.py (dumb byte pipe) ⇄ smolmaild
|
|||
devbox run serve
|
||||
```
|
||||
|
||||
Then open http://127.0.0.1:8096 and create an identity. A checklist over the mailbox carries the rest, each step opening the settings tab that completes it: back up the master, pin your server's public key (from the operator, through a trusted channel), and claim an address. Then fetch.
|
||||
Then open http://127.0.0.1:8096. Onboarding walks through one concern per screen: generate or restore an identity (the seed is shown once — write it down), pin a server's public key (from the operator, through a trusted channel), then claim an address on it. Then fetch.
|
||||
|
||||
For a server on a non-default port, start the bridge with `uv run bridge.py --allow-port <port>`.
|
||||
|
||||
|
|
@ -60,17 +60,18 @@ For a NixOS host, `nixosModules.default` provides `services.gsmol-bridge`:
|
|||
|
||||
The service runs under `DynamicUser`, restarts on failure, and — like the standalone script — listens on `127.0.0.1` by default. `nginx.enable` (or `caddy.enable`, wiring into `services.caddy.virtualHosts` — automatic HTTPS, no `enableACME`/`forceSSL` equivalent to set) puts a TLS-terminating reverse proxy in front rather than exposing the bridge directly: it has no TLS of its own, and a browser refuses a `ws://` connection from an `https://` page, so a real deployment needs one of the two regardless of `--allow-origin`. Enable only one; a deployment that replaces `services.caddy.configFile` with a hand-written Caddyfile instead of using `virtualHosts` needs the domain added there directly, proxying to `host`:`port`. `openFirewall = true` is for exposing the bridge's port directly instead of either, which is not the recommended path (see "Notes and limits").
|
||||
|
||||
`services.gsmol-bridge.presetServer = { host, publicKey }` bakes a server's key into the pages this deployment serves, so a first-time visitor is pre-pinned instead of asked to copy a base32 key into settings by hand (§4). The value reaches the browser over this deployment's own TLS, so it only stands in for "from the operator, through a trusted channel" when the deployment operator and the smolmaild operator are the same trusted party — set it only for a server you operate or otherwise vouch for. It seeds the first run only: the browser stores it as an ordinary pin from then on, replaceable or removable under settings → server like any other, and the checklist's pinning step is already ticked off on first run.
|
||||
`services.gsmol-bridge.presetServer = { host, publicKey }` bakes a server's key into the pages this deployment serves, so a first-time visitor is pre-pinned instead of asked to copy a base32 key into settings by hand (§4). The value reaches the browser over this deployment's own TLS, so it only stands in for "from the operator, through a trusted channel" when the deployment operator and the smolmaild operator are the same trusted party — set it only for a server you operate or otherwise vouch for. It seeds the first run only: the browser stores it as an ordinary pin from then on, same as one pinned through onboarding or a correspondent's server pinned later from compose's recovery card — and onboarding's server screen starts already on "Continue" instead of asking the visitor to fetch or paste anything.
|
||||
|
||||
## What it implements
|
||||
|
||||
- **Identity** (§2): one 32-byte master secret; create or restore. Every rotation index's signing seed and the accept key are derived from it with HKDF, so the X25519 agreement keys and every superseded identity are re-derivable rather than archived. The master lives in localStorage, mail stays sealed in IndexedDB — the browser profile is the trust boundary.
|
||||
- **Addressing** (§3): short `user@host` and self-certifying `smol://` addresses, base32 fingerprints.
|
||||
- **Trust** (§4, §8): server keys pinned on first trust, mismatch aborts the handshake; `RESOLVE` over an unpinned session is marked unverified; registration and fetching require a pinned server; `REGISTER` carries a proof-of-possession signature bound to the server's own key, so a binding cannot be replayed elsewhere.
|
||||
- **Trust** (§4, §8): server keys pinned on first trust, mismatch aborts the handshake; `RESOLVE` over an unpinned session is marked unverified; registration and fetching only ever reach a server onboarding already pinned; `REGISTER` carries a proof-of-possession signature bound to the server's own key, so a binding cannot be replayed elsewhere. An opt-in **require a pinned server** setting (settings → Advanced) refuses to learn a new correspondent's key from an unpinned server at all; compose shows the blocked host inline instead, with a fetch-and-pin-then-send recovery in place of losing the draft.
|
||||
- **Mail** (§5): sealed and signed envelopes, 1 KiB padding, frontmatter bodies, sent copies sealed to self, fetch pages forward by cursor and verifies id and signature before acknowledging — anything unreadable stays on the server. Outgoing mail carries a signed `Reply-To` frontmatter field with the sender's full `smol://` address (an "anonymous" checkbox omits it), so a first-time recipient can name the sender and answer; on receipt such a claim binds only when its key matches the message's signer, and never over an address already pinned to a different key.
|
||||
- **Accept tokens** (§5.8): mail from an accepted correspondent lands in the inbox's main tier; everything else lands in a separate "requests" tier. Accepting or blocking a contact pushes the changed token set to the server immediately; a learned token is attached automatically to outgoing mail once a correspondent's own token has been received.
|
||||
- **Leave mail on server**: a toggle under settings → mail that has fetch page forward by cursor without acknowledging, so mail stays server-side until deleted from the reader — off by default, which acknowledges (and so deletes) fetched mail immediately, as before.
|
||||
- **Leave mail on server**: a toggle under settings → Server → fetching that has fetch page forward by cursor without acknowledging, so mail stays server-side until deleted from the reader — off by default, which acknowledges (and so deletes) fetched mail immediately, as before.
|
||||
- **Rotation** (§7): rotate to the next index's derived key with a certificate both the old and new keys sign, binding the change to this username; contacts accept it from the chain; every superseded key stays derivable from the master, since mail sealed to it is readable with nothing else; key changes without a chain are surfaced, never applied silently.
|
||||
- **Settings**: four tabs — Identity (address, key, fingerprint, the recovery secret), Server (connection, timeout, fetching), Backup (export/import, sealed to the identity), Advanced (the pinning setting above, rotation, signing out).
|
||||
|
||||
## Layout
|
||||
|
||||
|
|
@ -81,12 +82,22 @@ web/js/noise.js Noise_NX_25519_ChaChaPoly_SHA256 initiator
|
|||
web/js/proto.js addresses, seal/unseal, frontmatter, rotation, op framing
|
||||
web/js/store.js localStorage + IndexedDB, sealed at rest
|
||||
web/js/app.js the UI
|
||||
web/js/i18n.js string lookup (t(), applyStaticLocale()) — see "Strings"
|
||||
web/js/config.js deploy-time server preset, empty by default (see "Deploy")
|
||||
web/locales/en-US.json generated string dictionary (see "Strings")
|
||||
locales/en-US/main.ftl vendored Fluent source (see "Strings")
|
||||
scripts/ build-time tooling, currently just the locale converter
|
||||
test/ vectors and end-to-end tests (below)
|
||||
```
|
||||
|
||||
The crypto is pure JavaScript rather than WebCrypto so the exact same modules run in Node for testing — WebCrypto has no ChaCha20-Poly1305 and no Ed25519→X25519 conversion, so half of it needed writing anyway.
|
||||
|
||||
## Strings
|
||||
|
||||
Every user-facing string lives in `locales/en-US/main.ftl`, a vendored copy of the same Fluent resource [carrier](../carrier) (the GTK4 client) uses — one shared source across both clients rather than two copies drifting apart. The file only ever uses Fluent's flat `id = value` shape (no selectors, terms, or attributes), so `scripts/build-locale.mjs` is a small, dependency-free converter rather than a real Fluent parser; it turns the `.ftl` into `web/locales/en-US.json`, the flat dictionary the browser actually fetches. `devbox run locale` regenerates it; the output is committed, not built at serve time.
|
||||
|
||||
`web/js/i18n.js` loads that dictionary once at boot. `t(id, args)` looks up a string and substitutes `{ $name }` placeholders; `applyStaticLocale()` does the same for `index.html`'s `data-l10n-id`/`-placeholder`/`-title`/`-aria-label` attributes. To change wording: edit `main.ftl` in carrier (the shared source, not this copy), copy the file over this repo's vendored copy, then `devbox run locale`.
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue