chore: untrack AGENTS.md, add license and AI declaration
This commit is contained in:
parent
4a89ca85e0
commit
403f65c3b6
5 changed files with 225 additions and 145 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@
|
|||
result
|
||||
result-*
|
||||
.claude/
|
||||
AGENTS.md
|
||||
|
|
|
|||
51
AGENTS.md
51
AGENTS.md
|
|
@ -1,51 +0,0 @@
|
|||
# General Working Rules
|
||||
|
||||
## Coding
|
||||
|
||||
- **Simple and idiomatic.** Write readable, conventional code. Prefer composition, immutability, explicit dependencies, and a single source of truth.
|
||||
- **Small and focused.** Keep functions and modules single-purpose. Separate concerns and avoid unnecessary nesting, abstraction, and global mutable state.
|
||||
- **Reuse existing solutions.** Follow project conventions and prefer existing tools and dependencies. Introduce new ones only when justified.
|
||||
- **Minimal changes.** Implement only what is requested. Prefer deletion over rewriting, and avoid unrelated refactoring, formatting, or speculative features.
|
||||
|
||||
## Comments
|
||||
|
||||
- **Self-contained.** Explain essential context inline without requiring external documents or conversations. References to nearby source code are acceptable.
|
||||
- **Concise.** Usually one sentence. Put longer explanations in brief module headers or documentation.
|
||||
- **Why, not what.** Comment only on non-obvious decisions, constraints, invariants, and surprising behavior. Do not restate obvious code.
|
||||
- **No unnecessary history.** Include implementation history only when essential to understanding current behavior. Remove obsolete comments.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Keep documentation concise, accurate, and synchronized with the implementation.
|
||||
- Distinguish implemented behavior from proposals and historical plans. Remove outdated information and shorten resolved findings.
|
||||
- Update specifications alongside changes to interfaces, formats, and contracts. Record important decisions where they are enforced.
|
||||
- **Natural reflow.** Write paragraphs as continuous lines without hard wrapping. Use blank lines between paragraphs and explicit line breaks only where structurally necessary.
|
||||
|
||||
## Working Style
|
||||
|
||||
- **Inspect first.** Examine existing code, configuration, architecture, and conventions rather than assuming how things work.
|
||||
- **Respect scope.** Follow the latest explicit instructions. Do not make unrelated changes or resume superseded tasks.
|
||||
- **Work silently.** No progress narration or request recaps. Interrupt only for genuine blockers or discoveries that materially change the approach.
|
||||
- **Resolve uncertainty.** Investigate missing information before asking. Never invent requirements or assume unverified behavior.
|
||||
- **Respect user decisions.** Identify significant design problems and propose concrete alternatives with trade-offs, but follow the user's choice.
|
||||
- **Respect authorization.** Observe approval requirements for changes, tests, commits, deployments, and destructive operations.
|
||||
- **Commit messages.** Use concise, single-line, tag-prefixed messages (e.g., fix:, feat:, refactor:, docs:). Describe what changed using the imperative mood.
|
||||
- **No credit notes.** Omit attribution, co-author trailers, AI-generated notices, and other credit statements from commits and generated documentation.
|
||||
|
||||
## Verification
|
||||
|
||||
- Test affected behavior using existing project tools. Add regression tests where appropriate.
|
||||
- Preserve safeguards for security, external input, data integrity, and concurrency.
|
||||
- Measure consequential assumptions before making architectural decisions.
|
||||
- Perform repository-wide cleanup or extensive refactoring only when explicitly requested.
|
||||
- Format and validate affected code without altering unrelated files.
|
||||
- Report what passed, failed, or could not be verified. Never claim untested behavior works.
|
||||
|
||||
## Completion
|
||||
|
||||
Summarize changes, verification results, important decisions, and unresolved issues in a few sentences. Avoid lengthy reports unless requested.
|
||||
|
||||
## Project Preferences
|
||||
|
||||
- use `git` colocated `jj` for version management
|
||||
- use `devbox` to manage dependencies and scripting
|
||||
11
AI-DECLARATION.md
Normal file
11
AI-DECLARATION.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
version: "0.1.2"
|
||||
level: copilot
|
||||
---
|
||||
|
||||
This format is based on [AI-DECLARATION.md](https://ai-declaration.md/en/0.1.2).
|
||||
|
||||
## Notes
|
||||
|
||||
- I directed the work step by step, reviewed and committed the results, and verified the UI live; build, packaging and cross-platform scripts.
|
||||
- Claude Code was used throughout, prompted and reviewed by me at each step. Its main task was UI and string refinements.
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
106
README.md
106
README.md
|
|
@ -1,16 +1,14 @@
|
|||
# gsmol
|
||||
|
||||
An HTML5 browser client for [Smol Mail](../smolmail), the minimalist end-to-end encrypted mail protocol: one 32-byte master secret, five operations, Noise_NX transport, sealed and signed messages, and accept tokens that sort correspondence from cold mail. Everything the reference CLI client does, in a browser tab.
|
||||
[](LICENSE) [](https://github.com/jetify-com/devbox/) [](https://ai-declaration.md) 
|
||||
|
||||
## Why there is a bridge
|
||||
|
||||
The protocol is raw TCP with a Noise_NX handshake, and browsers cannot open TCP sockets. So the client is split in two:
|
||||
[Smol Mail](../smolmail) in a browser tab. All the crypto, keys and mail handling run client-side in JavaScript; a small local bridge just relays bytes to a smolmaild server over WebSocket.
|
||||
|
||||
```
|
||||
browser (all crypto, all keys) ⇄ bridge.py (dumb byte pipe) ⇄ smolmaild (TCP :1961)
|
||||
```
|
||||
|
||||
`bridge.py` serves the web app and relays WebSocket bytes to the server untouched. It holds no keys, never parses a byte of traffic, and relays only to allowlisted ports (`--allow-port`, default 1961), so it is not an open proxy. It also relays only for upgrades whose `Origin` is its own page — WebSockets are exempt from CORS, so without that check any page the user happened to visit could drive the relay. `--allow-origin` admits others, and an upgrade carrying no `Origin` at all, which a browser never sends, is left alone for non-browser clients. The Noise session, sealing, signing, pinning and trust-on-first-use all run in the browser's JavaScript — the trust boundary is the machine running the browser, as with the CLI client's `identity.key` file.
|
||||
The bridge holds no keys and never parses a byte of traffic. The trust boundary is the machine running the browser.
|
||||
|
||||
## Run
|
||||
|
||||
|
|
@ -18,102 +16,22 @@ browser (all crypto, all keys) ⇄ bridge.py (dumb byte pipe) ⇄ smolmaild
|
|||
devbox run serve
|
||||
```
|
||||
|
||||
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.
|
||||
Open http://127.0.0.1:8096. Onboarding walks you through creating or restoring an identity (the seed is shown once — write it down), pinning your server's public key, and claiming an address. Then fetch.
|
||||
|
||||
For a server on a non-default port, start the bridge with `uv run bridge.py --allow-port <port>`.
|
||||
Non-default server port: `uv run bridge.py --allow-port <port>`.
|
||||
|
||||
## Deploy
|
||||
|
||||
`flake.nix` packages the bridge for a server, separately from `devbox`'s local dev loop:
|
||||
|
||||
```
|
||||
nix build .#default # -> ./result/bin/gsmol-bridge
|
||||
nix run . # runs it directly, default flags
|
||||
```
|
||||
|
||||
The package wraps `bridge.py` with `python3` and `aiohttp` from nixpkgs, not `uv` — no network access at build or run time, and the script's `#!/usr/bin/env -S uv run --script` shebang stays relevant only to `devbox run serve`.
|
||||
|
||||
For a NixOS host, `nixosModules.default` provides `services.gsmol-bridge`:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs.gsmol.url = "git+https://code.randogoth.com/randogoth/gsmol.git";
|
||||
|
||||
outputs = { self, nixpkgs, gsmol, ... }: {
|
||||
nixosConfigurations.mail = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
gsmol.nixosModules.default
|
||||
{
|
||||
services.gsmol-bridge = {
|
||||
enable = true;
|
||||
allowPorts = [ 1961 ]; # smolmaild's port(s)
|
||||
nginx.enable = true; # TLS via ACME, in front of the bridge
|
||||
nginx.domain = "mail.example.org";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
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, 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 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 → 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
|
||||
|
||||
```
|
||||
bridge.py web server + WebSocket relay (aiohttp, one file, no keys)
|
||||
web/js/crypto.js SHA-2, HKDF, ChaCha20-Poly1305, X25519, Ed25519 — dependency-free
|
||||
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
|
||||
## Test
|
||||
|
||||
```
|
||||
devbox run test
|
||||
```
|
||||
|
||||
1. `test/gen_vectors.py` regenerates `test/vectors.json` from the reference stack (hashlib/hmac, PyNaCl, noiseprotocol): hashes, AEAD, key agreement, signatures, the §2 conversions, master-derived rotation seeds and accept tokens, a fixed-ephemeral envelope, and a deterministic Noise_NX transcript with internal states.
|
||||
2. `test/vectors.test.mjs` checks the JS implementation byte for byte against those vectors, plus protocol cases: frontmatter (case-insensitive keys), addresses, and rotation chains bound to a username (accepted, broken, missing link, oversize, wrong username).
|
||||
3. `test/e2e.py` runs a live `smolmaild.py` and drives the reference client (Alice) against this client's JS stack (Bob, headless via `test/client.mjs`): registration, unsolicited mail landing in the requests tier, accepting a correspondent and the accept-token round trip that follows, fetch with acknowledgment, and a rotation whose chain the reference client walks. Bob's reply travels through the bridge's WebSocket relay — the exact byte path the browser takes.
|
||||
Byte-for-byte checks against the reference vectors, protocol cases, and a live end-to-end exchange with the reference client through the bridge — the exact byte path the browser takes. Only clicking through the UI is left to a manual pass.
|
||||
|
||||
What is not verified here: clicking through the actual browser UI. `devbox run serve` and a manual pass remain the last step; the logic under every button is what the tests exercise.
|
||||
## Deploy
|
||||
|
||||
## Notes and limits
|
||||
```
|
||||
nix build .#default # -> ./result/bin/gsmol-bridge
|
||||
```
|
||||
|
||||
- The master sits in localStorage: an XSS in the page or a compromised browser profile is game over, same as a stolen `identity.key` for the CLI client. The bridge serves a strict Content-Security-Policy (`default-src 'self'`, no inline script or style, no foreign origin) to raise that bar, but it is a mitigation and not a fix. No server push or notifications; click fetch.
|
||||
- Rotation is not revocation (§7): a stolen key can rotate onward and the chain validates. The UI surfaces rotations instead of applying them invisibly; out-of-band re-verification is the only defence.
|
||||
- The requests tier is a quota boundary the server enforces, not a filter: it does not refuse anyone's mail outright, which stays the reader's decision after decryption (§5.8).
|
||||
- The bridge binds to 127.0.0.1 by default and has no TLS of its own. Serving it beyond localhost directly is possible but unadvised: the relay is plaintext-agnostic (the traffic is already end-to-end encrypted, but the bridge would relay anyone's bytes to allowlisted ports). The NixOS module's `nginx.enable` puts a TLS-terminating reverse proxy in front instead, which is the deployment path "Deploy" above documents.
|
||||
- The relay is a proxy and is bounded as one, per the smolmail WebSocket annex: it relays only to allowlisted ports (`--allow-port`, checked per request) and refuses a WebSocket upgrade with a foreign `Origin`, so a page the user happens to visit cannot drive it. The annex's third bound is not implemented: `/tcp/{host}/{port}` dials any host the path names, including the loopback, private and link-local addresses of the machine the bridge runs on, which turns a public deployment into a door into the operator's own network. When implemented, the check belongs on the address the name resolved to, not on the name.
|
||||
The bridge binds to 127.0.0.1 and has no TLS of its own — for anything beyond localhost, put `nginx` or `caddy` in front. A NixOS module is included (`nixosModules.default` → `services.gsmol-bridge`) with the same options.
|
||||
Loading…
Add table
Add a link
Reference in a new issue