From 403f65c3b67b3bdb56346628c8979792a5aba732 Mon Sep 17 00:00:00 2001 From: randogoth Date: Fri, 9 Oct 2026 13:38:10 +0300 Subject: [PATCH] chore: untrack AGENTS.md, add license and AI declaration --- .gitignore | 1 + AGENTS.md | 51 ------------ AI-DECLARATION.md | 11 +++ LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 106 +++--------------------- 5 files changed, 225 insertions(+), 145 deletions(-) delete mode 100644 AGENTS.md create mode 100644 AI-DECLARATION.md create mode 100644 LICENSE diff --git a/.gitignore b/.gitignore index 148de57..38a6edb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ result result-* .claude/ +AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index eab6e03..0000000 --- a/AGENTS.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/AI-DECLARATION.md b/AI-DECLARATION.md new file mode 100644 index 0000000..360124f --- /dev/null +++ b/AI-DECLARATION.md @@ -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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c98d27d --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index 155dbbd..6014d63 100644 --- a/README.md +++ b/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: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Built with Devbox](https://www.jetify.com/img/devbox/shield_galaxy.svg)](https://github.com/jetify-com/devbox/) [![AI-DECLARATION: copilot](https://img.shields.io/badge/䷼%20AI--DECLARATION-copilot-fee2e2?labelColor=fee2e2)](https://ai-declaration.md) ![Nix Flake](https://img.shields.io/badge/Nix-Flake-5277C3?logo=nixos&logoColor=white) -## 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 `. +Non-default server port: `uv run bridge.py --allow-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. \ No newline at end of file