- Rust 96.3%
- Nix 3.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci / fmt / clippy / test / deny (push) Successful in 3m20s
The gateway shared a repo with the wasm core and the `<felis-terminal>` component, and shared nothing else with them: it links felis-client-core natively, they compile it to wasm, and the only thing crossing between them is `docs/broker.md` — a wire contract, not code. The old workspace was already paying for the pretence. Its Cargo.toml carried a `default-members` override whose comment says why: keeping the gateway out of the default set "stops `cargo test --target wasm32` from trying to build the tokio-bound gateway for wasm". Its CI needed separate, explicitly-named clippy steps for the same reason. That is a workspace working around holding two unrelated things. So: `git subtree split -P gateway`, which brings the 15 commits that touched this code with it. New here is the scaffolding a standalone repo needs — - a one-crate `[workspace]`, because `[lints] workspace = true` has to resolve somewhere. The lint tables are felis core's policy verbatim: a split is not a reason to hold a crate to a different bar. - a flake with no wasm32 target and no typescript-go, which is the first thing the split buys. This is a native tokio binary and never wanted either. - CI with one clippy step and one test step, for the same reason. - `docs/broker.md`, which comes here rather than staying with the browser: the server defines and enforces `felis-broker.v1`, and a client can only conform. Copied rather than subtree-split, so its history stays in felis-web-core — the one thing this split loses. Two source changes ride along, both because standing the repo up is what surfaced them, and neither is cosmetic enough to leave for later: - `rustls-pemfile` is gone. It is unmaintained (RUSTSEC-2025-0134) and its job has moved into `rustls::pki_types`, which rustls already pulls in — so `tls_acceptor` loses a direct dependency and its file handling at once. The dependency and the advisory both predate this split; what changed is that `cargo deny` now sees the crate as first-party, where the old layout had it a member's dep and let it through. - One assertion. The flake here locks a newer nightly whose clippy has learned `assert_is_empty`; its suggested rewrite then trips the workspace's own `trivial_casts`, so the form satisfying both is `Vec::<&str>::new()`. fmt / clippy -D warnings / 28 tests / deny: all green standalone. Assisted-by: Claude Code Opus 5 |
||
| .forgejo/workflows | ||
| docs | ||
| nix | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| clippy.toml | ||
| deny.toml | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
| rustfmt.toml | ||
felis-web-gateway
A WebSocket ↔ felis relay.
It attaches to a felis-daemon as a native client and re-exposes the
session's felis-protocol frames as JSON, so a browser can run a live,
bidirectional terminal without speaking felis's wire format.
The browser halves live elsewhere:
felis-web-component
(the <felis-terminal> element and its wasm grid core) and
felis-web (the app).
This repo and those share no code — only
docs/broker.md, the wire contract.
Two contracts on one listener
felis.v1— one WebSocket, one session. What an embedded<felis-terminal src="wss://…">uses.felis-broker.v1— one WebSocket, many sessions, plus a roster and a notification stream. What an app with tabs uses. The full contract, including why it is multiplexed rather than a socket per tab, isdocs/broker.md.
A client picks by offering the subprotocol; no offer means felis.v1.
It negotiates SCROLL_OP | ROW_DELTA_BATCH | SEARCH | CLI_OPS | NOTIFY | GRID_SIZE and reports the intersection with the daemon's own set in
its attach header — that intersection is what unlocks search, capture,
notifications and dynamic resize on the browser side. PULL_PACING is
deliberately withheld: the daemon would then wait for a per-vsync frame
request the browser never sends, and the view would freeze.
Run it
nix run . -- --socket "$XDG_RUNTIME_DIR/felis/daemon.sock" --listen 127.0.0.1:8771
Multiple daemons — the topology that makes a phone useful — are named rather than dialled by the client:
felis-web-gateway --socket "$XDG_RUNTIME_DIR/felis/daemon.sock" \
--host lab=[email protected] \
--listen 0.0.0.0:8771 \
--token-file ~/.config/felis/web-token
--host <name>=<ssh destination> builds a roster beside the implicit
local entry, reached over ssh <dest> felis-daemon relay. The browser
picks a roster key and never sends a destination, so this is not an
open ssh proxy and the keys stay on the gateway host.
Serving off-host
felis-web-gateway --socket … --listen 0.0.0.0:8771 \
--token-file ~/.config/felis/web-token \
--origin https://felis.example \
--tls-cert /path/fullchain.pem --tls-key /path/privkey.pem
This refuses to start if --listen is non-loopback and no token is set;
--insecure-no-auth is the opt-out for when something in front already
authenticates. The reason it refuses rather than warns: an open gateway
hands out shell sessions on every configured host, and its ssh keys are
what reach them. A VPN is not an auth boundary — phones get lost, and
other devices are on the VPN too.
--token-file rather than --token, because a command line is
world-readable in /proc. The token rides the Sec-WebSocket-Protocol
offer as felis-token.<value>: a WebSocket constructor cannot set
request headers, and the only alternative — a query parameter — puts the
credential in access logs, history and Referer.
--tls-cert/--tls-key serve wss:// in-process, so "phone on a VPN,
daemon at home" is one process rather than a reverse proxy in front of
one.
--ping-interval (25 s, 0 disables) keeps both relays alive through
idle-timeout proxies.
Develop
nix develop
cargo clippy --all-targets -- -D warnings
cargo test
The felis crates arrive as Cargo git dependencies pinned to one rev,
so Cargo.toml is the single place a felis version is chosen. That rev
has to stay in step with the one felis-web-component pins: the two halves
decode the same frames, and split repos cannot enforce that with a
lockfile.