From 2d3b1ea4c59baf09c846498541d53580930b0bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentino=20Me=C4=91imorec?= Date: Thu, 22 Oct 2026 12:58:29 +0200 Subject: [PATCH] Feeds: hosts and snippets from a URL; README demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A feed source reads a JSON document (version 1) with a token: its hosts (group, tags, jump hosts, key names, host keys, state) and read-only snippets, kept in step on refresh and every 15 minutes on desktops. Listed host keys are trusted for their host, others refused. New items from any cloud source get derived ids, and sync makes no conflict copies where only source-decided fields differ. Offboarding lists feed tokens. The README shows the app, drawn from the AppImage. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Valentino Međimorec --- CHANGELOG.md | 7 + Cargo.lock | 1 + README.md | 4 + ROADMAP.md | 3 + apps/desktop/e2e/README.md | 7 +- apps/desktop/e2e/appimage.mjs | 121 ++++ apps/desktop/e2e/demo.mjs | 132 ++++ apps/desktop/e2e/host-feed.mjs | 42 ++ apps/desktop/e2e/run.sh | 31 +- apps/desktop/src-tauri/src/bulk.rs | 15 +- apps/desktop/src-tauri/src/cloud.rs | 627 ++++++++++++++++-- apps/desktop/src-tauri/src/lib.rs | 4 + apps/desktop/src-tauri/src/offboard.rs | 13 +- apps/desktop/src-tauri/src/ssh.rs | 15 + apps/desktop/src-tauri/src/vault.rs | 140 ++++- apps/desktop/src/App.tsx | 78 ++- apps/desktop/src/CloudPage.tsx | 179 +++++- apps/desktop/src/Lists.tsx | 19 +- apps/desktop/src/RouteBar.tsx | 21 +- apps/desktop/src/SnippetCheck.tsx | 31 + apps/desktop/src/SnippetPanel.tsx | 58 +- apps/desktop/src/TeamsPanel.tsx | 6 +- apps/desktop/src/api.ts | 45 +- apps/desktop/src/mobile/MobileWorkspace.tsx | 7 +- apps/desktop/src/styles.css | 22 + assets/demo/morphgate.svg | 12 + crates/mg-inventory/src/feed.rs | 664 ++++++++++++++++++++ crates/mg-inventory/src/lib.rs | 12 +- crates/mg-inventory/src/shape.rs | 2 + crates/mg-inventory/src/vallic.rs | 2 + crates/mg-inventory/tests/feed.rs | 119 ++++ crates/mg-ssh/src/connection.rs | 50 ++ crates/mg-ssh/src/error.rs | 10 + crates/mg-ssh/src/lib.rs | 4 +- crates/mg-ssh/src/session.rs | 3 + crates/mg-ssh/tests/live.rs | 83 +++ crates/mg-store/Cargo.toml | 1 + crates/mg-store/src/history.rs | 1 + crates/mg-store/src/lib.rs | 7 +- crates/mg-store/src/model.rs | 45 +- crates/mg-store/src/moving.rs | 10 + crates/mg-store/src/sync.rs | 169 ++++- crates/mg-store/src/vault.rs | 88 ++- crates/mg-vpn/src/lib.rs | 1 + docs/README.md | 2 +- docs/architecture.md | 4 +- docs/development.md | 8 +- docs/features/cloud-sources.md | 128 +++- docs/features/hosts-groups-identities.md | 2 +- docs/features/offboarding.md | 3 +- docs/features/ssh-connections.md | 11 +- docs/features/sync.md | 1 + docs/features/terminal-and-snippets.md | 18 +- docs/features/vault-and-unlock.md | 2 +- docs/sync-crypto-spec.md | 6 +- tools/readme-demo/demo-svg.mjs | 156 +++++ tools/readme-demo/package-lock.json | 23 + tools/readme-demo/package.json | 10 + tools/readme-demo/snapshot.js | 164 +++++ tools/readme-demo/terminal.mjs | 164 +++++ 60 files changed, 3435 insertions(+), 178 deletions(-) create mode 100644 apps/desktop/e2e/demo.mjs create mode 100644 apps/desktop/e2e/host-feed.mjs create mode 100644 apps/desktop/src/SnippetCheck.tsx create mode 100644 assets/demo/morphgate.svg create mode 100644 crates/mg-inventory/src/feed.rs create mode 100644 crates/mg-inventory/tests/feed.rs create mode 100644 tools/readme-demo/demo-svg.mjs create mode 100644 tools/readme-demo/package-lock.json create mode 100644 tools/readme-demo/package.json create mode 100644 tools/readme-demo/snapshot.js create mode 100644 tools/readme-demo/terminal.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3599478..9bc32bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,13 @@ prompt. Write them for users. versions of the app keep syncing alongside this one. - Install and restart waits for an update check that's still running, so it installs the update that check found. +- Feeds: hosts and snippets from an address your team publishes (Hosts → Add from… → Feed URL…), + read with a token. They stay in step with it on their own, every 15 minutes; your renames, keys, + tags and notes stay yours. A feed can list each server's host key, so new servers connect + without asking you to trust them, and a server with another key is refused. Its snippets are + read-only, and when the feed changes one, Run and Paste show you the new commands first. +- Two devices refreshing the same cloud source at once no longer make duplicate hosts or + "(conflict)" copies. ## 0.2.0 diff --git a/Cargo.lock b/Cargo.lock index f2ec5f1..e574ae6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3397,6 +3397,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "sha2 0.11.1", "tempfile", "thiserror 2.0.21", "tokio", diff --git a/README.md b/README.md index a3407be..1dc998b 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ encrypted sync you can self-host. Docs: [developer docs](docs/README.md) · [sync & crypto spec](docs/sync-crypto-spec.md) · [self-host](https://morphgate.com/docs/self-hosting) · [roadmap](ROADMAP.md) · [releasing](RELEASING.md) · [changelog](CHANGELOG.md) +

+ MorphGate: the Hosts page with hosts in groups; a click on web-1 opens its session, where uptime, git log and ls run +

+ ## Install Download from the [Releases](../../releases) page: diff --git a/ROADMAP.md b/ROADMAP.md index 3c17081..c1a792c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -40,6 +40,9 @@ xterm.js in the webview avoids writing a terminal renderer. - [x] Terminal appearance: built-in themes plus themes imported from Ghostty, Alacritty, iTerm2 and Windows Terminal files (vault items, synced), a theme per host, and this device's font, size and cursor - [x] Jump hosts (ProxyJump chains via direct-tcpip, per-hop host-key pinning) +- [x] Cloud sources: hosts kept in step with Vallic Cloud (its API), AWS, Google Cloud, Azure, Hetzner Cloud and + DigitalOcean (their CLIs), and feeds: hosts and snippets from a JSON document a team publishes at a URL, + read with a token, refreshed every 15 minutes, with the host keys it lists trusted for its hosts - [x] Port forwarding: local / remote / dynamic SOCKS5 tunnels saved in the vault, start/stop from the sidebar - [x] SFTP tabs (`russh-sftp`): local + remote panes, drag and drop (also from the OS), recursive upload/download with progress, cancel and replace, mkdir/rename/chmod/delete diff --git a/apps/desktop/e2e/README.md b/apps/desktop/e2e/README.md index cfad6aa..c9827f1 100644 --- a/apps/desktop/e2e/README.md +++ b/apps/desktop/e2e/README.md @@ -4,10 +4,11 @@ These run the real AppImage, deb and rpm binaries, not a dev build, and drive th | Scenario | What it checks | | --- | --- | -| `appimage.mjs` | Creates a vault; SSHes to a throwaway local sshd and runs a command; imports two hosts and their key from a `~/.ssh/config` (one behind the other as its jump host) and connects through it; imports a PuTTY session with a new key in a `.ppk` file, a Termius-style CSV file with a group and a Royal TS document, and signs in with the PuTTY key; connects Vallic Cloud through a stand-in console (`vallic-console.mjs`: device sign-in, rotating tokens, an environment that is this sshd) and signs in to its host; signs in with a key that only has a certificate from the run's user CA (which sshd trusts), from the vault and from an agent; reaches a host by a command its jump host runs (with a login command instead of the shell), and another by a proxy command on this computer (both the relay in `crates/mg-ssh/tests/relay.py`); selects two hosts, moves them into a group and deletes them; signs in to a host with one key from an `ssh-agent` (its own socket) and forwards it (`ssh-add -l` on the host lists it); splits a tab into three panes (beside and below, one through a jump host), broadcasts a command to all of them and then types into one, drags a border and closes them; opens a `morphgate://connect` link from a second launch (which hands it over and exits) and connects from the filled-in quick connect, and refuses an unknown link; refuses a tampered update twice without touching the AppImage; installs the signed update, restarts, unlocks the same vault as the new version, and finds nothing newer | +| `appimage.mjs` | Creates a vault; SSHes to a throwaway local sshd and runs a command; imports two hosts and their key from a `~/.ssh/config` (one behind the other as its jump host) and connects through it; imports a PuTTY session with a new key in a `.ppk` file, a Termius-style CSV file with a group and a Royal TS document, and signs in with the PuTTY key; connects Vallic Cloud through a stand-in console (`vallic-console.mjs`: device sign-in, rotating tokens, an environment that is this sshd) and signs in to its host; adds AWS and DigitalOcean through stand-in CLIs (`clouds/`); adds a feed (`host-feed.mjs`) whose host connects with the host key it lists, whose wrongly listed host is refused and whose changed snippet asks before it runs; signs in with a key that only has a certificate from the run's user CA (which sshd trusts), from the vault and from an agent; reaches a host by a command its jump host runs (with a login command instead of the shell), and another by a proxy command on this computer (both the relay in `crates/mg-ssh/tests/relay.py`); selects two hosts, moves them into a group and deletes them; signs in to a host with one key from an `ssh-agent` (its own socket) and forwards it (`ssh-add -l` on the host lists it); splits a tab into three panes (beside and below, one through a jump host), broadcasts a command to all of them and then types into one, drags a border and closes them; opens a `morphgate://connect` link from a second launch (which hands it over and exits) and connects from the filled-in quick connect, and refuses an unknown link; refuses a tampered update twice without touching the AppImage; installs the signed update, restarts, unlocks the same vault as the new version, and finds nothing newer | | `package.mjs` (deb, rpm) | The binary knows how it was installed, finds the update, and warns that installing asks for an administrator password (installing needs root, so it stops there) | | `teams.mjs` | Two apps side by side (Alice and Bob) with a local reference sync server: both turn on sync; Alice creates an organization and a vault and invites Bob; Bob joins only after the owner's fingerprint matches, and Alice confirms him only after his matches; a host saved in the shared vault reaches Bob, his edit reaches Alice; Bob publishes his SSH key; Alice revokes him (the vault leaves his device) and rotates the vault key; members may create vaults; the activity log tells the story. Then Alice offboards Bob against the local sshd: removal and key rotation, his key found and removed from `authorized_keys`, and the host's shared key replaced (she still logs in) | | `layout.mjs` | Turns sync on from the offer a new vault gets, fills every page (hosts in a group, a key, a snippet, a tunnel, a team with an invite to a long address), and at 860, 1024, 1280 and 1600 px wide checks that nothing on a page overlaps or runs out of its column. Closing an editor returns to the page it was opened from, and closing a page to the session under it; with no session open, a page takes the tab bar's place | +| `demo.mjs` | Only with `E2E_ONLY=demo`: the README's demo. The AppImage's Hosts page, a click on a host and its recorded session (in a demo shell), drawn by `tools/readme-demo/demo-svg.mjs` as `demo/morphgate.svg` (copy it to `assets/demo/morphgate.svg`) | | `mobile.mjs` | The phone layout in a 390 × 844 window: imports a key and saves a host on the phone screens, connects from the host's sheet, interrupts a command with Ctrl from the extra-key row and repeats one with ↑, keeps the session open behind the host list, browses into a folder over SFTP and downloads a file to Downloads, then locks and unlocks on the phone unlock screen | ```sh @@ -24,10 +25,10 @@ Screenshots and logs go to `target/e2e/run-