diff --git a/README.md b/README.md index ea6157da..4d7a35cc 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Depth lives in [`docs/`](docs/) — pick the one matching your task: | config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) | | what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) | | naming, wire protocol, commit style | [`docs/conventions.md`](docs/conventions.md) | +| nginx vhost map + sub-domain routing | [`docs/gateway.md`](docs/gateway.md) | | NixOS / nspawn gotchas | [`docs/gotchas.md`](docs/gotchas.md) | ## Host config diff --git a/docs/gateway.md b/docs/gateway.md new file mode 100644 index 00000000..698d681c --- /dev/null +++ b/docs/gateway.md @@ -0,0 +1,78 @@ +# hive-gateway + +Single nginx in front of every hyperhive web surface. Container `hive-gateway`, shared host netns, system-config (not meta-flake managed). Configured via `services.hyperhive.gateway.*` + per-subsystem opt-in flags in `services.hyperhive.{forge,matrix,...}`. + +## Vhost map + +| URL | vhost | upstream | source | +| --- | --- | --- | --- | +| `/` | `_` (catch-all) | hive-c0re dashboard (`7000`) | always | +| `/agent//` | `_` | per-agent harness on `agent_web_port(name)` | `agentPortsFile` JSON, #15 | +| `/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null`, #660 / #747 | +| `/matrix/` (deprecated) | `_` | 301 → `matrix./` | `matrix.gui.enable`, #772 | +| `forge./` | `forge.` | forgejo (`3000`) | `forge.behindGateway`, #754 | +| `matrix./_matrix/*` | `matrix.` | tuwunel (`8008`) | `matrix.gatewayHost != null`, #764 | +| `matrix./` | `matrix.` | fluffychat-web static | `matrix.gui.enable`, #772 | +| `matrix./config.json` | `matrix.` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null`, #736 | + +Per-agent UIs stay sub-path because they're hyperhive-internal and base-path-aware (iris #731). External standard apps (forge / matrix) get sub-domains because their defaults work cleanly at sub-domain root + per-origin cookies / storage isolation matters. + +## Discovery flow (matrix) + +Operator points client at ``. Sequence: + +1. Client fetches `http:///.well-known/matrix/client` → `{"m.homeserver":{"base_url":"http://matrix."}}` (no port suffix when gateway listens on 80). +2. Client connects to `matrix./_matrix/client/...`. +3. Gateway routes `/_matrix/*` → tuwunel at `127.0.0.1:8008`. + +Federation peers fetch `.well-known/matrix/server` → `{"m.server":"matrix."}` and connect to `matrix.:8448` per spec default. Gateway only listens on configured `port`; cross-hive federation needs either an SRV record (`_matrix._tcp.matrix.` → port 80) OR `matrix.openFirewall = true` so peers reach tuwunel's federation port directly. Hyperhive is mostly closed/internal, so this rarely bites. + +## SPA fallback (Accept-header pattern) + +The `` catch-all and the `matrix.` vhost both serve a flutter SPA (per-agent UI, fluffychat). Two requirements collide: + +- hard-refresh on a sub-route must serve `index.html` (SPA's client-side router takes over after JS bootstrap) +- missing assets must surface as 404, not as HTML with wrong content-type (the original #643 bug) + +Solution: an `nginx http`-context `map $http_accept $matrix_spa_target { ... }` keyed on the request's Accept header. Browser navigations (`Accept: text/html,...`) get `index.html`; asset fetches (`Accept: image/*`, `*/*`, etc.) get a sentinel nonexistent path → `try_files` falls through to `=404`. No extension allowlist, no `if` block, no regex heuristics. #686 + #729 thread for the design history. + +## Local dev (`localHostsEntry`) + +`services.hyperhive.gateway.localHostsEntry = true` adds entries to the host's `/etc/hosts`: + +- `` → `127.0.0.1` +- `forge.` → `127.0.0.1` (when forge.behindGateway) +- `matrix.` → `127.0.0.1` (when matrix.gatewayHost set) + +`lib.unique` de-dupes if any sub-domain happens to equal another entry. Operators with real DNS leave it off. + +## Sub-domain shape (rationale) + +mara verdict at #749:9609 + #747:9722: sub-domain over sub-path for forge + matrix, sub-path for per-agent UIs. + +- forgejo's default `ROOT_URL = http:///` works without any `X-Forwarded-Prefix` gymnastics — sub-domain hosting is the canonical Forgejo deploy shape. +- matrix-spec deployments universally use `matrix.` for the actual API listener — federation already expects this. +- per-agent UIs are hyperhive-internal; iris's #731 made them base-path-aware specifically for `/agent//`. Sub-domain per agent would multiply DNS + TLS-per-subdomain cost without per-app config wins. +- cookie / storage isolation: a future forge XSS can't reach the dashboard session because they're different origins. + +`services.hyperhive.{forge.domain,matrix.gatewayHost}` take the full hostname (`forge.darkest.space`, `git.example.com`) rather than a label that gets concatenated with hive-domain — mara on #754:9684 wanted operator control over the full shape, not a forced `