mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-01 17:38:15 +00:00
Update readme
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
# MeshTender
|
||||
|
||||
A Go web app for tending [MeshCore](https://meshcore.co.uk) meshes. MeshTender holds a single
|
||||
server-wide MeshCore identity (Ed25519 + X25519). Repeater owners grant it admin by running
|
||||
`setperm <server_pubkey> 3` on their repeater. Because every action flows through the one server
|
||||
identity, MeshTender can mediate *which users* may control a repeater — so you can share a repeater
|
||||
with other people without ever handing out keys.
|
||||
A Go web app for tending [MeshCore](https://meshcore.io/) meshes — sharing repeater
|
||||
administration with other people, keeping a site's documentation and maintenance history with the
|
||||
node instead of in someone's head, and publishing an organization's recommended configuration so
|
||||
every repeater in a mesh is set up the same way.
|
||||
|
||||
## How the hardware confirm/control path works
|
||||
MeshTender holds a single server-wide MeshCore identity (Ed25519 + X25519). A repeater owner grants
|
||||
it admin by running `setperm <server_pubkey> 3` on their repeater. Because every action flows
|
||||
through that one server identity, MeshTender can mediate *which users* may control a repeater — so
|
||||
a repeater can be shared without ever handing out keys, and access can be revoked instantly.
|
||||
|
||||
## How the hardware control path works
|
||||
|
||||
The server owns all MeshCore crypto, but the radio (a MeshCore **KISS modem**) is plugged into the
|
||||
*user's* computer. So:
|
||||
@@ -18,77 +22,101 @@ server ──(WebSocket, KISS bytes)──▶ browser ──(WebSerial)──
|
||||
|
||||
The server builds a signed `AnonReq` login packet, KISS-frames it, and streams the bytes to the
|
||||
browser, which writes them to the modem over WebSerial. The reply travels back the same way for the
|
||||
server to decrypt — proving MeshTender can reach the repeater. Confirmation is optional; the
|
||||
repeater works either way. This is implemented as a custom `hardware.Transport`
|
||||
(`internal/wsbridge`) over a WebSocket.
|
||||
server to decrypt. Nothing is stored on the user's machine and no key ever leaves the server; the
|
||||
modem is only a transport. This is a custom `hardware.Transport` (`internal/wsbridge`) over a
|
||||
WebSocket.
|
||||
|
||||
**Mesh-friendly transmission.** Each session uses strictly increasing per-command timestamps (the
|
||||
repeater dedupes same-timestamp commands) and a 1-message/second rate limit, so a user can't flood
|
||||
the shared LoRa mesh through their modem. The first contact (login) floods with 3-byte routing path
|
||||
hashes for reliable propagation; once the repeater's reply reveals the route home, subsequent
|
||||
commands use **direct routing** along that path — traversing only those repeaters instead of
|
||||
flooding the whole mesh — with automatic fallback to flood if the path goes stale. Sends, retries,
|
||||
timestamps, and routing all live in one `mesh.Exchanger`.
|
||||
|
||||
There is also a **serial setup path** for a brand-new node: the browser talks the repeater's own
|
||||
plain-text CLI directly over USB. The repeater's private key is generated in the browser and never
|
||||
sent to the server — only its public key is (`internal/core/repeater_setup.go`).
|
||||
|
||||
## What it does
|
||||
|
||||
**Repeaters.** Add a node (over LoRa via a modem, or from scratch over USB serial), send firmware
|
||||
CLI commands from the **console**, and read the per-repeater audit log of who sent what and when.
|
||||
Location is opt-in per repeater, as is appearing on an org's public map, as is publishing a
|
||||
read-only public page at the repeater's stable public ID.
|
||||
|
||||
**Registry.** Each repeater carries site **documentation** (a public section and an internal one)
|
||||
and a **maintenance history** anyone with access can log. Ownership can be **transferred** to a
|
||||
designated steward, so the docs, history, and public URL survive the original builder moving on.
|
||||
|
||||
**Sharing.** Owners create single-use, labeled, expiring **invite links** — one per person. The
|
||||
recipient signs in and accepts, consuming the link; used links stay as an audit trail. There is no
|
||||
user directory. Each shared person gets an explicit per-command allowlist (deny by default).
|
||||
|
||||
**Organizations.** Any user creates an org and becomes its admin; orgs are publicly listed and any
|
||||
signed-in user can join and be promoted. A repeater participates in every org its owner belongs to
|
||||
unless the owner excludes it, and the owner can optionally restrict which commands a given org may
|
||||
run on a given repeater. Effective permissions = the site catalog's per-tier ceiling ∩ the owner's
|
||||
optional per-repeater opt-in ∩ the caller's tier (admin ⊇ member). Orgs also get a description,
|
||||
links, markdown content, a repeater map, and optionally a verified custom domain.
|
||||
|
||||
**Configuration ("desired state").** An org publishes named **profiles** (base settings) plus
|
||||
**regions** — a named hierarchy of geofences that compile into MeshCore `region def` chains, with
|
||||
per-region and root flood policy (`region allowf` / `region denyf`). A repeater's coordinates select
|
||||
every region whose polygon contains it. The console's "apply organization configuration" flow shows
|
||||
the resulting command list — profile steps plus region commands — marking any line the user can't
|
||||
run and why. Regions are drawn on a Leaflet/Geoman map editor.
|
||||
|
||||
**Command catalog.** Every repeater CLI command, seeded from the firmware and modeled per-parameter
|
||||
(`set.tx`, `set.radio`, …) with feature/operation grouping, a `risky` tag, and per-tier org flags.
|
||||
An owner can always run anything on their own node; the flags are the ceiling for everyone else.
|
||||
|
||||
**Accounts.** Username-only (no required email), an optional public profile at `/u/{username}`
|
||||
(display name, bio, links — not indexed), timezone,
|
||||
passkeys (WebAuthn via `go-webauthn`) with a bcrypt password fallback, sessions via `scs`. Email is
|
||||
optional and used only for recovery (verification + password reset) via Resend; without a
|
||||
`MAIL_FROM` the whole feature is hidden, and without an API key messages are logged instead of sent.
|
||||
|
||||
**Admin.** Instance capabilities (`cap_manage_users`, `cap_manage_catalog`) — the first registered
|
||||
account is bootstrapped with both — plus first-party traffic analytics (no third party, no PII;
|
||||
visitors counted by a daily-rotating salted hash), CSP violation reports, a reverse-proxy test page,
|
||||
and encrypted export/restore of the server identity.
|
||||
|
||||
## Stack
|
||||
|
||||
- Go 1.26, [meshcore-go](https://github.com/meshcore-go/meshcore-go) for the MeshCore protocol/crypto
|
||||
- Postgres (pgx + goose migrations)
|
||||
- Accounts are username-only (no email/PII), with an optional display name; passkey auth (WebAuthn
|
||||
via `go-webauthn`) with a password fallback (bcrypt); sessions via `scs`
|
||||
- Sharing is via **single-use share links** — the owner mints one labeled link per person; the
|
||||
recipient signs in and accepts (consuming it). No user directory; used links are kept as an audit
|
||||
trail showing who accepted
|
||||
- **Mesh-friendly transmission**: each session uses strictly increasing per-command timestamps (the
|
||||
repeater dedupes same-timestamp commands) and a 1-message/second rate limit, so a user can't flood
|
||||
the shared LoRa mesh through their modem. The first contact (login) floods with 3-byte routing path
|
||||
hashes (reliable propagation); once the repeater's reply reveals the route home, subsequent commands
|
||||
use **direct routing** along that path — traversing only those repeaters instead of flooding the
|
||||
whole mesh — with automatic fallback to flood if the path goes stale. All sends, retries, timestamps,
|
||||
and routing are handled by one `mesh.Exchanger`
|
||||
- **Command console**: authorized users send firmware CLI commands to a repeater over their modem
|
||||
(same WebSocket↔WebSerial bridge as confirm). Owners run anything; shared users run only the
|
||||
commands the owner granted them. Every send is recorded in a per-repeater audit log (who/when/ack)
|
||||
- **Command catalog**: all repeater CLI commands, seeded from the firmware, modeled per-parameter
|
||||
(`set.tx`, `set.radio`, …) with a `risky` tag and default-set flags. There is no global on/off —
|
||||
the owner can run anything; the flags seed what *others* are offered
|
||||
- **Instance capabilities** (not tiers): `cap_manage_users` (grant/revoke capabilities) and
|
||||
`cap_manage_catalog` (edit the catalog), managed at `/admin`. The first registered account is
|
||||
bootstrapped with both. These are separate from per-repeater access (sharing)
|
||||
- **Organizations** (the trust-first tier above sharing): any user creates an org and becomes its
|
||||
admin; orgs are publicly listed and any signed-in user joins from the org page and can be promoted. An org has a **versioned,
|
||||
two-tier (admin/member) permission policy**. An owner *contributes* a repeater to an org and
|
||||
**consents** to the policy version; effective commands = the org's *current* set ∩ the version the
|
||||
owner *consented to*, per the user's tier (admins ⊇ members). Policy additions require the owner to
|
||||
re-consent (shown as a diff + changelog); removals apply immediately. Org-admins operate distant
|
||||
nodes over the mesh from their own modem. Withdraw / leave revokes access instantly. Members reach
|
||||
contributed repeaters from the org page; the org page also shows a **map** of repeater locations.
|
||||
- **Repeater location** (opt-in): when adding a repeater the owner may consent to storing its lat/lon,
|
||||
which is fetched (`get lat`/`get lon`) during the modem test and shown on org maps. Off by default.
|
||||
- Server-rendered `html/template` + htmx; hand-written JS only for the WebSerial page
|
||||
- `coder/websocket`
|
||||
Go single binary, server-rendered `html/template` + htmx, PostgreSQL. No SPA, no bundler, no asset
|
||||
build step — migrations, templates, and static assets are all `go:embed`ed.
|
||||
|
||||
- Go 1.26.5 (pinned exactly — see "Verifying a build"), [meshcore-go](https://github.com/meshcore-go/meshcore-go)
|
||||
for the MeshCore protocol/crypto
|
||||
- Postgres via `pgx` with raw SQL (no ORM) and goose migrations; `chi` router; `coder/websocket`
|
||||
- Hand-written JS only where the platform demands it (WebSerial, Leaflet maps, small delegated
|
||||
handlers in `ui.js`)
|
||||
|
||||
### Vendored front-end assets
|
||||
|
||||
The strict CSP forbids external scripts/styles (`*-src 'self'`), so all third-party
|
||||
front-end libraries are **self-hosted** in `internal/web/static/` (embedded into the
|
||||
binary) rather than loaded from a CDN. They're served content-hash fingerprinted, with
|
||||
a one-year `immutable` Cache-Control, and pre-compressed (gzip + brotli) by the asset
|
||||
manifest in `internal/web/assets.go`.
|
||||
The strict CSP forbids external scripts/styles (`*-src 'self'`), so every third-party front-end
|
||||
library is **self-hosted** in `internal/web/static/` and embedded into the binary. They're served
|
||||
content-hash fingerprinted with a one-year `immutable` Cache-Control and pre-compressed (gzip +
|
||||
brotli) by the asset manifest in `internal/web/assets.go`. The only external resource anywhere is
|
||||
CARTO map tiles, allowlisted in `img-src`.
|
||||
|
||||
Current pinned versions:
|
||||
Which libraries, at which versions, from which upstream artifact, is recorded once in
|
||||
`internal/licenses/manifest.go` — with a SHA-256 per file — and rendered into
|
||||
`THIRD-PARTY-NOTICES.md`. That manifest is the list; it isn't duplicated here, because a second
|
||||
copy would only go stale. Its tests fail if a file in `internal/web/static/` is neither declared
|
||||
there nor listed as first-party, so a new library can't slip past the audit.
|
||||
|
||||
| Library | Version | Files |
|
||||
|---|---|---|
|
||||
| [htmx](https://htmx.org) | 2.0.10 | `htmx.min.js` |
|
||||
| [Leaflet](https://leafletjs.com) | 1.9.4 | `leaflet.js`, `leaflet.css` |
|
||||
| [Leaflet-Geoman](https://geoman.io) | 2.20.0 | `leaflet-geoman.js`, `leaflet-geoman.css` |
|
||||
| [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster) | 1.5.3 | `leaflet.markercluster.js`, `leaflet.markercluster.css` |
|
||||
| [Tabler](https://tabler.io) | 1.4.0 | `tabler.min.js`, `tabler.min.css` |
|
||||
|
||||
**Updating a vendored library:** fetch the minified build from jsdelivr (mirrors npm
|
||||
exactly), e.g. `https://cdn.jsdelivr.net/npm/htmx.org@<version>/dist/htmx.min.js`, and
|
||||
overwrite the file in `internal/web/static/` (keep the existing filename). **Strip any
|
||||
trailing `sourceMappingURL` comment** (`//# sourceMappingURL=…` / `/*# … */`) — we don't
|
||||
self-host the `.map` files, so the comment only produces a 404 when devtools are open.
|
||||
Then update the version above and validate in a browser (`mise run e2e`, which fails on
|
||||
any CSP violation).
|
||||
**Updating one:** fetch the minified build from jsdelivr (mirrors npm exactly), e.g.
|
||||
`https://cdn.jsdelivr.net/npm/htmx.org@<version>/dist/htmx.min.js`, and overwrite the file (keep the
|
||||
filename). **Strip any trailing `sourceMappingURL` comment** — we don't self-host `.map` files, so
|
||||
it only 404s with devtools open — and **keep the copyright banner** minifiers like to drop; MIT and
|
||||
BSD require it. Then update the version and hash in the manifest, run `mise run licenses --update`,
|
||||
and validate with `mise run e2e`.
|
||||
|
||||
## Running locally
|
||||
|
||||
One process serves all three hosts (root, auth, app) over TLS. Dev can't use `*.localhost` —
|
||||
One process serves all hosts (root, www, auth, app) on one port. Dev can't use `*.localhost` —
|
||||
`localhost` is a public suffix, so a WebAuthn RP ID of `localhost` is rejected from a subdomain and
|
||||
passkeys won't work — so dev uses a real registrable domain whose subdomains resolve to `127.0.0.1`,
|
||||
with a locally-trusted [mkcert](https://github.com/FiloSottile/mkcert) certificate. `.env.example`
|
||||
@@ -98,18 +126,32 @@ is preconfigured for `leighthaus.dev`; point its subdomains at `127.0.0.1` (real
|
||||
```sh
|
||||
docker compose up -d # Postgres
|
||||
|
||||
# One-time: trust a local CA and mint a cert for the dev domain + its subdomains.
|
||||
# One-time: trust a local CA and create a cert for the dev domain + its subdomains.
|
||||
brew install mkcert && mkcert -install
|
||||
mkcert -cert-file ./certs/dev.pem -key-file ./certs/dev-key.pem "*.leighthaus.dev" leighthaus.dev
|
||||
|
||||
cp .env.example .env # then set MESHTENDER_MASTER_KEY=$(openssl rand -hex 32)
|
||||
set -a; . ./.env; set +a
|
||||
go run ./cmd/meshtender # migrates on boot; serves HTTPS on :8080
|
||||
mise run dev # migrates on boot; serves HTTPS on :8080
|
||||
```
|
||||
|
||||
Then open <https://app.leighthaus.dev:8080> (dashboard) or <https://leighthaus.dev:8080> (public
|
||||
root). WebSerial requires a secure context, which the mkcert HTTPS above provides — a real deployment
|
||||
must likewise serve the confirm/control pages over HTTPS.
|
||||
root). WebSerial requires a secure context, which the mkcert HTTPS provides — a real deployment must
|
||||
likewise serve the console pages over HTTPS.
|
||||
|
||||
`mise run seed` fills the database with realistic fake data; `mise run reset` truncates everything
|
||||
except users with credentials, passkeys, sessions, and the server identity. (Both are `go run
|
||||
./cmd/meshtender --seed|--reset` under the hood.)
|
||||
|
||||
### mise tasks
|
||||
|
||||
| Task | What it does |
|
||||
|---|---|
|
||||
| `mise run dev` | run the server |
|
||||
| `mise run lint` | `golangci-lint run` (staticcheck + gosec, config in `.golangci.yml`) |
|
||||
| `mise run seed` / `reset` | seed fake data / truncate non-account data |
|
||||
| `mise run e2e` | browser tests in a throwaway headless-shell container (`--run <regex>` to filter) |
|
||||
| `mise run licenses` | audit dependency licenses (`--update` rewrites `THIRD-PARTY-NOTICES.md`) |
|
||||
| `mise run image` | build the OCI image with ko and print its digest (`--load` to run it locally) |
|
||||
|
||||
### Configuration (env)
|
||||
|
||||
@@ -123,9 +165,14 @@ must likewise serve the confirm/control pages over HTTPS.
|
||||
| `MESHTENDER_TLS_CERT` / `_TLS_KEY` | cert/key for in-process HTTPS (see mkcert above); omit only behind a TLS-terminating proxy |
|
||||
| `MESHTENDER_RP_ID` / `_RP_ORIGIN` / `_RP_NAME` | WebAuthn relying-party settings (must line up with the hosts) |
|
||||
| `MESHTENDER_TRUSTED_PROXIES` | proxies whose `X-Forwarded-For`/`X-Real-IP` are trusted when resolving the client IP — comma-separated CIDRs/IPs, or `private` for the RFC1918/link-local/ULA ranges. Loopback is always trusted. Verify with the admin **Reverse proxy test** page. |
|
||||
| `MESHTENDER_MAIL_FROM` / `_MAIL_REPLY_TO` | enables the optional recovery-email feature; unset ⇒ no email UI at all |
|
||||
| `MESHTENDER_RESEND_API_KEY` | enables real delivery; unset ⇒ messages are logged to stderr instead (the dev default) |
|
||||
|
||||
> **Note:** `MESHTENDER_MASTER_KEY` is coupled to the stored identity — changing it makes the
|
||||
> existing `server_identity` row undecryptable. Keep it stable.
|
||||
> existing `server_identity` row undecryptable. Keep it stable, and keep a copy of the admin
|
||||
> identity export (which stays sealed under this key).
|
||||
|
||||
Cross-host cookie and session rules are documented in [`docs/auth-cross-host.md`](docs/auth-cross-host.md).
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -133,27 +180,44 @@ must likewise serve the confirm/control pages over HTTPS.
|
||||
go test ./...
|
||||
```
|
||||
|
||||
That's it — **just make sure Docker is running.** DB-backed tests (store queries and the
|
||||
end-to-end confirm/console round-trips) spin up a throwaway `postgres:17` container automatically
|
||||
via [testcontainers](https://golang.testcontainers.org/). The harness migrates a single template
|
||||
database once, then clones a fresh database per test (`CREATE DATABASE … TEMPLATE …`), so every
|
||||
test gets pristine, isolated state and tears it down when it finishes. No env vars, no manual
|
||||
database setup, nothing to wipe.
|
||||
That's it — **just make sure Docker is running.** DB-backed tests spin up a throwaway `postgres:17`
|
||||
container automatically via [testcontainers](https://golang.testcontainers.org/). `internal/testdb`
|
||||
migrates a single template database once, then clones a fresh database per test
|
||||
(`CREATE DATABASE … TEMPLATE …`), so every test gets pristine, isolated state and can run in
|
||||
parallel. No env vars, no manual setup, nothing to wipe.
|
||||
|
||||
To run against an **existing** Postgres instead of a container (this is how CI reuses its service
|
||||
container), set `MESHTENDER_TEST_DATABASE_URL` to a DSN on that server. The connecting role needs
|
||||
`CREATEDB`, and the harness only ever creates/drops its own `mt_tmpl_*` / `mt_test_*` databases:
|
||||
To run against an **existing** Postgres instead of a container, set
|
||||
`MESHTENDER_TEST_DATABASE_URL` to a DSN on that server. The role needs `CREATEDB`,
|
||||
and the harness only ever creates/drops its own `mt_tmpl_*` / `mt_test_*` databases:
|
||||
|
||||
```sh
|
||||
MESHTENDER_TEST_DATABASE_URL="postgres://meshtender:meshtender@localhost:5432/postgres?sslmode=disable" \
|
||||
go test ./internal/core/ -run TestConfirmRoundTrip -v
|
||||
go test ./internal/core/ -run TestConsole -v
|
||||
```
|
||||
|
||||
**Browser tests** live in `internal/e2e/` behind a `//go:build browser` tag, so `go test ./...`
|
||||
never needs a browser. `mise run e2e` starts a `chromedp/headless-shell` container (no local Chrome
|
||||
install), runs the suite against an in-process server, and fails on any CSP violation. If the
|
||||
container isn't up, they skip rather than fail.
|
||||
|
||||
## Third-party licenses
|
||||
|
||||
Every dependency has to be one we can actually comply with when we ship a binary and an image, so
|
||||
dependencies are limited to permissive licenses — the allowed set is `AllowedSPDX` in
|
||||
`internal/licenses/manifest.go`, and copyleft terms (GPL, LGPL, AGPL, MPL, SSPL) are out, including
|
||||
in the test tree. Attribution obligations are met by `THIRD-PARTY-NOTICES.md` and by keeping the
|
||||
copyright banners in the vendored front-end files.
|
||||
|
||||
`mise run licenses` enforces it: it scans the whole Go module graph (binary, test, and
|
||||
`browser`-tagged) with `google/licensecheck`, plus the manifest of things Go tooling can't see —
|
||||
vendored front-end files, icon artwork, the base image, external services — and fails if anything
|
||||
falls outside the allowed set or if `THIRD-PARTY-NOTICES.md` has drifted.
|
||||
|
||||
## Building the image
|
||||
|
||||
There is no Dockerfile. MeshTender is a pure-Go single binary with migrations, templates, and
|
||||
static assets embedded via `go:embed`, so [ko](https://ko.build) compiles it and lays it straight
|
||||
onto a base image — no build context and no Docker daemon. The build is configured in `.ko.yaml`.
|
||||
There is no Dockerfile. MeshTender is a pure-Go single binary with everything embedded, so
|
||||
[ko](https://ko.build) compiles it and lays it straight onto a base image — no build context and no
|
||||
Docker daemon. Configured in `.ko.yaml`.
|
||||
|
||||
```sh
|
||||
mise run image # build and print the image digest (pushes nothing)
|
||||
@@ -162,15 +226,13 @@ mise run image --load # load into the local Docker daemon so you can run it
|
||||
|
||||
## Verifying a build
|
||||
|
||||
The published image is **reproducible**: you can rebuild it yourself and confirm you get the same
|
||||
digest we shipped, rather than taking our word that the image matches this source. Every build
|
||||
input is pinned — the Go toolchain (`GOTOOLCHAIN` in `.config/mise/config.toml`), the ko version,
|
||||
and the base image *by digest* rather than by its floating `:nonroot` tag — and ko zeroes the layer
|
||||
timestamps. `TestReleasePinsAreConsistent` and `TestBaseImageIsPinnedByDigest`
|
||||
The published image is **reproducible**: you can rebuild it and confirm you get the digest we
|
||||
shipped, rather than taking our word that the image matches this source. Every build input is
|
||||
pinned — the Go toolchain (`GOTOOLCHAIN` in `.config/mise/config.toml`), the ko version, and the
|
||||
base image *by digest* rather than by its floating `:nonroot` tag — and ko zeroes layer timestamps.
|
||||
`TestReleasePinsAreConsistent` and `TestBaseImageIsPinnedByDigest`
|
||||
(`internal/licenses/reproducible_test.go`) fail the build if any of those pins drift apart.
|
||||
|
||||
To reproduce a release:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/jleight/meshtender && cd meshtender
|
||||
git checkout v1.2.3 # the tag you are verifying
|
||||
@@ -181,28 +243,33 @@ mise run image
|
||||
Compare the printed `sha256:…` against the digest of the published image. The registry name is not
|
||||
part of the digest, so this works without any access to our registry.
|
||||
|
||||
Two things will change the digest, and both are intentional:
|
||||
Two things change the digest, and both are intentional:
|
||||
|
||||
- **The checkout must be clean and at the exact commit.** Go stamps the commit SHA, the commit
|
||||
time, and a dirty-tree flag into the binary, so an edited or differently-tagged tree produces a
|
||||
different digest. That is a feature — it binds the image to a specific commit — but it does mean
|
||||
`git status` must be empty before you build.
|
||||
- **The platform must match.** `mise run image` targets `linux/amd64` by default; pass
|
||||
time, and a dirty-tree flag into the binary, so an edited tree produces a different digest. That
|
||||
binds the image to a specific commit — but it does mean `git status` must be empty first.
|
||||
- **The platform must match.** `mise run image` targets `linux/amd64`; pass
|
||||
`--platform linux/arm64` to verify that variant.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
cmd/meshtender main / wiring
|
||||
internal/config env config
|
||||
internal/store Postgres + goose migrations + queries
|
||||
internal/identity load-or-generate server identity, AES-GCM seal at rest
|
||||
internal/mesh build AnonReq login packet, decode repeater RESPONSE
|
||||
internal/auth WebAuthn + password + sessions
|
||||
internal/wsbridge WebSocket ↔ hardware.Transport bridge
|
||||
internal/web routing, templates, static assets, confirm orchestration
|
||||
cmd/meshtender main / wiring; --seed and --reset
|
||||
cmd/licenses the license auditor behind `mise run licenses`
|
||||
internal/config env config
|
||||
internal/store Postgres: goose migrations + raw SQL queries
|
||||
internal/testdb ephemeral per-test database (template clone)
|
||||
internal/identity load-or-generate server identity, AES-GCM seal at rest, backup envelope
|
||||
internal/mesh AnonReq login packet, RESPONSE decode, exchanger (routing, retries, rate limit)
|
||||
internal/wsbridge WebSocket ↔ hardware.Transport bridge to the browser's KISS modem
|
||||
internal/geo GeoJSON containment/overlap for region geofences
|
||||
internal/web HTTP foundation: renderer, middleware, host dispatcher, CSP, static assets
|
||||
internal/auth auth host: WebAuthn + password + sessions + account management
|
||||
internal/marketing root host: landing, docs, legal, public org/repeater discovery
|
||||
internal/core app host: repeaters, console, orgs, configuration, sharing, admin
|
||||
internal/analytics first-party request tracking + daily rollups
|
||||
internal/mail Resend delivery, with a logging fallback
|
||||
internal/licenses dependency license audit + non-Go manifest + reproducibility pins
|
||||
internal/seed fake data for local testing
|
||||
internal/e2e chromedp browser tests (build tag `browser`)
|
||||
```
|
||||
|
||||
## Known follow-ups
|
||||
|
||||
- (none currently)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -303,26 +302,3 @@ func TestNoticesUsesUnixLineEndings(t *testing.T) {
|
||||
t.Errorf("%s contains a carriage return — run `mise run licenses --update`", NoticesPath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadmeVendoredVersionsMatchManifest keeps the README's version table from
|
||||
// drifting, since it is the second place a version is written down.
|
||||
func TestReadmeVendoredVersionsMatchManifest(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
b, err := os.ReadFile(filepath.Join(root, "README.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading README.md: %v", err)
|
||||
}
|
||||
readme := string(b)
|
||||
|
||||
for _, d := range Deps {
|
||||
if d.Kind != KindAsset || d.Version == "" {
|
||||
continue
|
||||
}
|
||||
// The table renders as: | [Name](url) | version | files |
|
||||
row := fmt.Sprintf("| %s |", d.Version)
|
||||
if !strings.Contains(readme, row) {
|
||||
t.Errorf("README.md has no vendored-assets table row stating version %s for %s; "+
|
||||
"the table and the manifest disagree", d.Version, d.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user