Compare commits

..
4 changed files with 408 additions and 229 deletions

View file

@ -42,7 +42,6 @@ Depth lives in [`docs/`](docs/) — pick the one matching your task:
| config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) | | config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) |
| what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) | | what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) |
| naming, wire protocol, commit style | [`docs/conventions.md`](docs/conventions.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) | | NixOS / nspawn gotchas | [`docs/gotchas.md`](docs/gotchas.md) |
## Host config ## Host config

View file

@ -1,78 +0,0 @@
# 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 |
| --- | --- | --- | --- |
| `<hive>/` | `_` (catch-all) | hive-c0re dashboard (`7000`) | always |
| `<hive>/agent/<name>/` | `_` | per-agent harness on `agent_web_port(name)` | `agentPortsFile` JSON, #15 |
| `<hive>/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null`, #660 / #747 |
| `<hive>/matrix/` (deprecated) | `_` | 301 → `matrix.<hive>/` | `matrix.gui.enable`, #772 |
| `forge.<hive>/` | `forge.<hive>` | forgejo (`3000`) | `forge.behindGateway`, #754 |
| `matrix.<hive>/_matrix/*` | `matrix.<hive>` | tuwunel (`8008`) | `matrix.gatewayHost != null`, #764 |
| `matrix.<hive>/` | `matrix.<hive>` | fluffychat-web static | `matrix.gui.enable`, #772 |
| `matrix.<hive>/config.json` | `matrix.<hive>` | 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 `<hive>`. Sequence:
1. Client fetches `http://<hive>/.well-known/matrix/client``{"m.homeserver":{"base_url":"http://matrix.<hive>"}}` (no port suffix when gateway listens on 80).
2. Client connects to `matrix.<hive>/_matrix/client/...`.
3. Gateway routes `/_matrix/*` → tuwunel at `127.0.0.1:8008`.
Federation peers fetch `.well-known/matrix/server``{"m.server":"matrix.<hive>"}` and connect to `matrix.<hive>:8448` per spec default. Gateway only listens on configured `port`; cross-hive federation needs either an SRV record (`_matrix._tcp.matrix.<hive>` → 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 `<hive>` catch-all and the `matrix.<hive>` 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`:
- `<hive-domain>``127.0.0.1`
- `forge.<hive>``127.0.0.1` (when forge.behindGateway)
- `matrix.<hive>``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://<host>/` works without any `X-Forwarded-Prefix` gymnastics — sub-domain hosting is the canonical Forgejo deploy shape.
- matrix-spec deployments universally use `matrix.<server_name>` 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/<name>/`. 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 `<label>.<hive-domain>` pattern.
## Tuning knobs
Per-vhost timeouts + body-size limits live in the location blocks:
- forge `/` (forgejo): `client_max_body_size 1G` (LFS), `proxy_read_timeout 1h` (multi-GB clones), `proxyWebsockets = true` (live-update endpoints).
- matrix `/_matrix/` (tuwunel): `client_max_body_size 50M` (media uploads), `proxy_read_timeout 1h` (long-poll `/sync`), CORS `*` (federation + cross-origin clients), `proxyWebsockets = true`.
- per-agent `/agent/<name>/`: `proxy_read_timeout 1d` (long-lived SSE / WebSocket dashboards), `proxyWebsockets = true`, `X-Forwarded-Prefix` set so the harness can build absolute URLs when relative isn't enough.
SSH for forge stays direct on `cfg.sshPort` — separate listener protocol, not HTTP-over-nginx.
## Sequencing history
- #15 v0 (per-agent routing, #740) — first sub-app behind the gateway, JSON port table from c0re.
- #686 / #729 — Accept-header SPA fallback pattern.
- #749 / #754 — forge to sub-domain (mara: sub-domain over sub-path).
- #747 / #764 — matrix sub-domain vhost + `.well-known` delegation.
- #772 / #775 — fluffychat hops from `<hive>/matrix/` to `matrix.<hive>/`.
Next-up tracked separately: #14 (container netns isolation), TLS (#594).

View file

@ -10,10 +10,25 @@ let
matrixCfg = config.services.hyperhive.matrix; matrixCfg = config.services.hyperhive.matrix;
forgeCfg = config.services.hyperhive.forge; forgeCfg = config.services.hyperhive.forge;
# Per-agent port table for `/agent/<name>/` routing. C0re writes # Per-agent port table for `/agent/<name>/` routing (#15 v0). Single-
# this JSON on every topology change; gateway reads at deploy time. # sourced from `cfg.agentPortsFile` (default
# Missing file → empty map → no per-agent routes (graceful default). # `/var/lib/hyperhive/agent-ports.json`), written by hive-c0re on every
# See `docs/gateway.md` for the discovery + rebuild flow. # topology change in shape `{ "<name>": <port>, ... }`.
#
# Read at deploy time via `builtins.fromJSON (builtins.readFile ...)`
# — pure eval (the file lives outside the nix store; nix copies the
# content into the store as a fixed-output dep). When the file is
# missing (fresh install before c0re has had a chance to write it),
# default to an empty map → no per-agent routes generated → gateway
# falls back to its pre-#15 shape. The container rebuilds on every
# `hivectl gateway-sync` (operator-initiated) or on the next
# `nixos-rebuild switch`, picking up whatever c0re has written
# since the last build.
#
# mara on #740 (comment 9295) + #15 (comment 9270): the gateway
# nginx container lives in system config (not meta), so it can't
# auto-rebuild from meta-flake events — the JSON file is what
# bridges the host's nix eval to the agent-lifecycle data c0re owns.
agentPortsTable = agentPortsTable =
if cfg.agentPortsFile == null || !builtins.pathExists cfg.agentPortsFile then if cfg.agentPortsFile == null || !builtins.pathExists cfg.agentPortsFile then
{ } { }
@ -21,11 +36,21 @@ let
builtins.fromJSON (builtins.readFile cfg.agentPortsFile); builtins.fromJSON (builtins.readFile cfg.agentPortsFile);
in in
{ {
# Single nginx in front of every hyperhive web surface — dashboard, # Single nginx in front of every hyperhive surface (#609 / #15 v0).
# per-agent UIs (sub-path), forge + matrix (sub-domain), .well-known # Lives in its own nixos-container (like hive-forge / hive-matrix) so
# delegations. Container `hive-gateway`, shared host netns, # the operator can opt out without touching the host's own nginx, and
# state-free. Full vhost map + discovery flow + design rationale in # so the static-serve responsibility for the matrix GUI moves off
# `docs/gateway.md`. # hive-c0re's axum router. Shares host netns so `localhost`
# upstream resolution works without any port-forward dance.
#
# Routes (v0):
# `location /matrix/` → static-serve fluffychat-web dist (when
# `services.hyperhive.matrix.gui.enable` is true)
# `location /` → proxy_pass to hive-c0re's dashboard upstream
#
# Container name `hive-gateway` keeps hive-c0re's lifecycle scanner
# (which only sees `h-*`) out of the picture. State-free — nginx
# config lives in the nix store, no runtime persistence to manage.
options.services.hyperhive.gateway = { options.services.hyperhive.gateway = {
enable = lib.mkOption { enable = lib.mkOption {
@ -184,15 +209,33 @@ in
enable = true; enable = true;
recommendedProxySettings = true; recommendedProxySettings = true;
recommendedOptimisation = true; recommendedOptimisation = true;
# Accept-header SPA fallback (#686 / #729): navigations # SPA-fallback target keyed on the `Accept` request header
# (`Accept: text/html,...`) fall to index.html, asset # (#686, mara + damocles on PR #729). This decides whether a
# fetches (Accept *anything else*) fall to a sentinel # `/matrix/...` miss falls through to `index.html` (route
# nonexistent path → `try_files` returns 404. Pattern # navigation) or returns a clean 404 (asset miss) — see the
# detailed in `docs/gateway.md` ("SPA fallback"). # `/matrix/` location comment below for the full rationale.
#
# Top-frame browser navigations always send
# `Accept: text/html,...` (chrome/firefox/safari are
# consistent on this). Asset fetches from script tags / img
# / fetch() / XHR send asset-typed Accepts (`image/*`,
# `application/javascript`, `*/*`) without `text/html`.
# Mapping is purely on the header → no extension allowlist
# to keep in sync with whatever the SPA ships, no regex
# heuristic to false-positive on dot-segment routes.
#
# `$matrix_spa_target` defaults to a sentinel nonexistent
# path so `try_files` falls through to the trailing `=404`
# for asset misses. Browser navigations route to
# `/matrix/index.html` where the SPA's client-side router
# takes over.
#
# Only emitted when the matrix GUI is on (saves a no-op
# `map` directive otherwise).
appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) '' appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) ''
map $http_accept $matrix_spa_target { map $http_accept $matrix_spa_target {
default "/__matrix_spa_no_html_fallback"; default "/__matrix_spa_no_html_fallback";
"~*text/html" "/index.html"; "~*text/html" "/matrix/index.html";
} }
''; '';
virtualHosts = { virtualHosts = {
@ -204,37 +247,99 @@ in
} }
]; ];
locations = locations =
# `<hive>/matrix/*` → 301 → `matrix.<hive>/$1` # Matrix GUI: when the operator has flipped both
# (fluffychat moved to sub-domain root in #772; this # `services.hyperhive.matrix.enable` and `matrix.gui.enable`
# keeps bookmarks + deep-links working during the # on, nginx serves fluffychat-web (or whatever override)
# transition). See `docs/gateway.md` for the vhost # as a static dist at `/matrix/`.
# map. #
lib.optionalAttrs ( # SPA fallback (iris/#643, rewritten in #686 per mara
matrixCfg.enable # + damocles on PR #729): the original
&& matrixCfg.gui.enable # `try_files $uri $uri/ /matrix/index.html;` shape
&& matrixCfg.gatewayHost != null # silently masked missing assets — flutter's bootstrap
) ( # requesting e.g. `/matrix/native_executor.js` got
let # `index.html` (Content-Type: text/html, status 200)
portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}"; # when the file was absent from the dist, so the JS
target = "http://${matrixCfg.gatewayHost}${portSuffix}"; # runtime never loaded and `/matrix/` rendered blank
in # without any visible error.
{ #
"/matrix/" = { # The followup #729 narrowed it with an extension
extraConfig = '' # allowlist; this version uses `$matrix_spa_target`
rewrite ^/matrix/(.*)$ ${target}/$1 permanent; # (defined in the `appendHttpConfig` above, keyed on
''; # the `Accept` header) so the decision lives in HTTP
}; # semantics rather than a maintained extension list.
} # Navigations (Accept: text/html) fall to index.html;
) # asset fetches (Accept: */*, image/*, etc.) get a
# clean 404 via the trailing `=404`.
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable) {
"/matrix/" = {
alias = "${matrixCfg.gui.package}/";
extraConfig = ''
try_files $uri $uri/ $matrix_spa_target =404;
'';
};
# FluffyChat fetches `/matrix/config.json` directly on
# boot for its own branding + default-homeserver
# bootstrap, BEFORE asking the user to pick a server
# (#736). The upstream `pkgs.fluffychat-web` dist
# ships without one, so the fetch 404s and the user
# sees the empty "enter homeserver" prompt. Serve a
# minimal config that pre-fills `defaultHomeserver`
# with the operator's hive domain — the matrix
# client then runs `.well-known/matrix/client` against
# that domain (already served by the
# `= /.well-known/matrix/client` block below) and
# discovers the actual tuwunel endpoint.
#
# Only the `defaultHomeserver` field is overridden —
# everything else (branding, audio defaults, etc.)
# falls back to fluffychat's hardcoded defaults so
# we don't pin against upstream config-schema drift.
# No-op when `services.hyperhive.domain` is unset
# (the location block is omitted entirely; the SPA
# then falls back to its empty form, same as before
# #736).
} // lib.optionalAttrs (
matrixCfg.enable && matrixCfg.gui.enable && hyperhiveDomain != null
) {
"= /matrix/config.json" = {
extraConfig = ''
default_type application/json;
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
'';
};
}
// //
# `.well-known/matrix/{client,server}` discovery JSON. # `.well-known/matrix/*` auto-discovery (#660): when
# Points clients at `matrixCfg.gatewayHost` (sub-domain # `services.hyperhive.matrix.enable` is on and the
# vhost) when set; falls back to direct `<hive>:<httpPort>` # operator's set a hive domain, the gateway serves
# when no gateway target. CORS `*` per matrix spec. # the matrix-spec discovery JSON at the canonical
# See `docs/gateway.md` "Discovery flow" for the full # location so clients pointed at `${hyperhive.domain}`
# client-bootstrap sequence. # resolve through to the actual tuwunel endpoint
# without needing a `matrix.` subdomain.
#
# `m.homeserver.base_url` advertises the client-server
# API. `m.server` advertises the federation
# `host:port` (tuwunel serves both client + federation
# on the same `httpPort` — see hive-matrix.nix).
#
# CORS `*` on the client endpoint per the matrix spec
# (https://spec.matrix.org/v1.15/client-server-api/#getwell-knownmatrixclient).
# No-op until the operator turns matrix on; until then
# there's no homeserver to advertise.
lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) ( lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) (
let let
# `.well-known/matrix/{client,server}` advertise where
# the actual matrix API lives. When `matrixCfg.gatewayHost`
# is set (default `matrix.<hive-domain>`, #747), point
# at the sub-domain — no port suffix when the gateway
# is on the canonical port 80, transparent to clients
# (mara on #749:9609 sub-domain verdict, "not user-
# visible because the .well-known redirect routes
# clients through automatically"). When `gatewayHost`
# is unset (no hive-domain, or operator nulled it),
# fall back to the direct `host:port` shape — clients
# reach tuwunel without going through the gateway,
# no sub-domain delegation.
portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}"; portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}";
clientBaseUrl = clientBaseUrl =
if matrixCfg.gatewayHost != null then if matrixCfg.gatewayHost != null then
@ -264,12 +369,33 @@ in
} }
) )
// //
# Per-agent UIs (#15 v0). One `/agent/<name>/` block # Per-agent UIs (#15 v0). One `/agent/<name>/`
# per entry in `agentPortsTable`. Trailing-slash pair # block per `<name>: <port>` entry in
# strips the prefix; `X-Forwarded-Prefix` lets the # `agentPortsTable` (loaded from `cfg.agentPortsFile`
# harness build absolute URLs when relative isn't # — `/var/lib/hyperhive/agent-ports.json` by default,
# enough. See `docs/gateway.md` for the vhost map # written by hive-c0re on every topology change).
# + tuning rationale. #
# Trailing-slash pair (`/agent/<name>/` + `proxy_pass
# http://...:<port>/`) strips the `/agent/<name>`
# prefix on the upstream side, so the agent server
# receives `GET /` for the SPA root, `GET /api/state`
# for the API, `GET /screen/ws` for the websocket, etc.
# The agent's emitted asset URLs are document-relative
# (iris's #731) so they round-trip back through the
# gateway under the same prefix without the harness
# needing prefix-awareness.
#
# `X-Forwarded-Prefix` set so the harness can build
# correct absolute URLs for any case where relative
# isn't enough (server-emitted redirects, OG meta
# tags, etc.).
#
# SSE / websocket support via `proxyWebsockets = true`
# (same as the c0re `/` block below).
#
# Empty / missing `cfg.agentPortsFile` → empty table
# → no per-agent blocks; old `<host>:<port>/` direct
# reach still works.
lib.mapAttrs' (name: port: { lib.mapAttrs' (name: port: {
name = "/agent/${name}/"; name = "/agent/${name}/";
value = { value = {
@ -299,11 +425,31 @@ in
}; };
} }
// //
# Forge sub-domain vhost (#749 / #754). `server_name = # Forge vhost (#749, mara verdict at issue:9609 —
# forge.domain`, proxies all `/` → forgejo. Tuned for # sub-domain over sub-path). When forgejo runs behind the
# git: `client_max_body_size 1G`, `proxy_read_timeout 1h` # gateway (`forge.behindGateway = true`), it gets its own
# (multi-GB clones). SSH stays direct on `forge.sshPort`. # `server { server_name = forge.domain; }` block. The
# See `docs/gateway.md`. # block proxies all `/` → `http://127.0.0.1:<forge.httpPort>/`
# so forgejo handles requests at root (default deploy shape
# — no `ROOT_URL`-prefix translation needed).
#
# `forge.domain` is the full hostname (e.g.
# `forge.darkest.space`, `git.example.com`) — single source
# of truth for both the forgejo `DOMAIN` setting and the
# gateway vhost name (mara on #754:9684 — "specify full
# forge domain in options instead").
#
# `client_max_body_size 1G` — git pushes + LFS uploads can
# be large; nginx's default 1M would 413 most real commits.
#
# Long timeouts for big repo operations: a fresh clone of a
# multi-GB repo can take minutes; the default 60s
# `proxy_read_timeout` would abort mid-stream.
#
# `proxyWebsockets = true` keeps forgejo's live-update
# endpoints (`/api/v1/events`) + any future websocket
# endpoints working transparently. SSH stays direct on
# `forge.sshPort` (separate listener protocol, not HTTP).
lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) { lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) {
"${forgeCfg.domain}" = { "${forgeCfg.domain}" = {
listen = [ listen = [
@ -325,12 +471,34 @@ in
}; };
} }
// //
# Matrix sub-domain vhost (#747 / #764). `server_name = # Matrix homeserver vhost (#747, mara verdict on #749:9609 —
# matrixCfg.gatewayHost`. `/_matrix/*` → tuwunel (CORS *, # sub-domain over sub-path for matrix; "not user-visible"
# 50M body cap, 1h long-poll timeout). `/` serves # because clients discover the sub-domain via the
# fluffychat (#772) or 404 if GUI off. nginx # `.well-known/matrix/{client,server}` delegation served
# longer-prefix-wins puts `/_matrix/` ahead of `/`. # above on the bare hive-domain).
# See `docs/gateway.md`. #
# `server { server_name = matrixCfg.gatewayHost; }` proxies
# `/_matrix/...` → `http://127.0.0.1:''${matrixCfg.httpPort}/_matrix/...`.
# Tuwunel listens on `:''${httpPort}` (default 8008); the
# gateway terminates on `:''${cfg.port}` (80) so external
# clients speak matrix over the canonical web port without
# operators having to open the tuwunel port through firewalls.
#
# `/` returns 404 — nothing else lives at the matrix vhost;
# the matrix client-server API is entirely under `/_matrix/`,
# and federation under `/_matrix/federation/...`.
#
# CORS `*` on the matrix vhost per the matrix spec —
# federation + client requests come from any origin.
#
# `client_max_body_size 50M` covers typical media uploads
# (matrix-spec media size cap default); operators with bigger
# uploads override via the matrix module's own cap when that
# lands.
#
# `proxy_read_timeout 1h` for long-poll `/sync`; the default
# 60s would abort `/sync?timeout=30000` legitimately when
# tuwunel's keepalive exceeds that.
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) { lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) {
"${matrixCfg.gatewayHost}" = { "${matrixCfg.gatewayHost}" = {
listen = [ listen = [
@ -339,48 +507,22 @@ in
port = cfg.port; port = cfg.port;
} }
]; ];
locations = locations = {
{ "/_matrix/" = {
"/_matrix/" = { proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}";
proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}"; proxyWebsockets = true;
proxyWebsockets = true; extraConfig = ''
extraConfig = '' proxy_buffering off;
proxy_buffering off; client_max_body_size 50M;
client_max_body_size 50M; proxy_read_timeout 1h;
proxy_read_timeout 1h; proxy_send_timeout 1h;
proxy_send_timeout 1h; add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Origin *; '';
'';
};
}
// lib.optionalAttrs (matrixCfg.gui.enable) (
{
# fluffychat at sub-domain root, SPA-fallback via
# the Accept-header `$matrix_spa_target` map.
"/" = {
alias = "${matrixCfg.gui.package}/";
extraConfig = ''
try_files $uri $uri/ $matrix_spa_target =404;
'';
};
}
// lib.optionalAttrs (hyperhiveDomain != null) {
# FluffyChat boot-config pre-fill so the client's
# `.well-known/matrix/client` lookup hits the
# right delegation endpoint (#736).
"= /config.json" = {
extraConfig = ''
default_type application/json;
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
'';
};
}
)
// lib.optionalAttrs (!matrixCfg.gui.enable) {
"/" = {
return = "404";
};
}; };
"/" = {
return = "404";
};
};
}; };
}; };
}; };
@ -391,10 +533,17 @@ in
allowedTCPPorts = [ cfg.port ]; allowedTCPPorts = [ cfg.port ];
}; };
# `/etc/hosts` entries for local dev — bare hive domain + any # `/etc/hosts` entries for local dev: the bare hive domain plus
# sub-domain modules that are on. `lib.unique` dedupes if any # any sub-domain modules (forge via #749/#754, matrix via #747)
# sub-domain happens to equal another. See `docs/gateway.md` # that are on. All map to `127.0.0.1` since the gateway shares
# ("Local dev"). # host netns. Operators with real DNS leave `localHostsEntry =
# false`; this is the dev-loop shortcut for `http://<hive-domain>/`
# + `http://forge.<hive-domain>/` + `http://matrix.<hive-domain>/`
# resolving locally.
#
# `lib.unique` collapses any duplicate (e.g. if forge.domain
# happens to equal hyperhiveDomain or matrixCfg.gatewayHost) so
# `/etc/hosts` doesn't carry the same entry twice.
networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) { networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) {
"127.0.0.1" = lib.unique ( "127.0.0.1" = lib.unique (
[ hyperhiveDomain ] [ hyperhiveDomain ]

View file

@ -9,20 +9,50 @@ let
hyperhiveDomain = config.services.hyperhive.domain; hyperhiveDomain = config.services.hyperhive.domain;
effectiveServerName = if cfg.serverName != null then cfg.serverName else hyperhiveDomain; effectiveServerName = if cfg.serverName != null then cfg.serverName else hyperhiveDomain;
# Three files are missing from `pkgs.fluffychat-web` because # Three files are missing from nixpkgs's `pkgs.fluffychat-web` dist
# `flutter341.buildFlutterApplication` doesn't run the dart # because `flutter341.buildFlutterApplication` doesn't run the dart
# web-worker compile pass + doesn't run the native_imaging emscripten # web-worker compile pass + doesn't run the native_imaging package's
# build (#685). `fluffychat-web-imaging` below builds the latter from # emscripten build (#685):
# source via `pkgs.emscripten`; the worker compile is inline in #
# `fluffychat-web-fixed.postInstall`. Drop both when nixpkgs's # - native_executor.js ← flutter web worker entry, compiled
# flutter builder grows worker + emcc support upstream. # from web/native_executor.dart via
# `dart compile js` (handled inline
# in `fluffychat-web-fixed.postInstall`
# below — dart SDK is already in the
# flutter341 closure)
#
# - Imaging.js / Imaging.wasm ← emscripten-compiled C library from
# the native_imaging dart package
# (vendored by Famedly). The package
# ships C source + a Makefile that
# builds them via emcc; nixpkgs's
# flutter builder doesn't run that
# pipeline. Built from source via
# `fluffychat-web-imaging` below
# (per mara's #685 call: "fix the
# compile … dont use the prebuilt
# binary").
#
# When `flutter341.buildFlutterApplication` grows worker + emcc
# support upstream, drop both this derivation and the postInstall.
# `Imaging.{js,wasm}` built from `native_imaging`'s C source via # Imaging.{js,wasm} built from source: native_imaging's `js/Makefile`
# emscripten. Source comes from # runs `emcmake cmake` → `make -C build` → `emcc` to produce the
# `pkgs.fluffychat-web.passthru.pubspecLock.dependencySources` so # emscripten-wrapped C library that fluffychat's main.dart.js
# there's no parallel hash pin — version auto-syncs with nixpkgs # references at runtime.
# bumps. Build closure +~3.6 GiB (emscripten LLVM); runtime closure #
# is just the two output files. # Source: the exact native_imaging derivation that `pkgs.fluffychat-web`
# already pulls in via its `pubspecLock` (resolved by nixpkgs's flutter
# pub-cache machinery), reached via `passthru.pubspecLock.dependencySources`.
# This means **no parallel hash pin** — when nixpkgs bumps
# `pkgs.fluffychat-web` (and with it the pubspec.lock-resolved
# native_imaging version), our build automatically picks up the
# matching source. Version is also pulled from passthru for the
# derivation's `version` attr so it stays in lockstep.
#
# Closure cost: `pkgs.emscripten` is ~3.6 GiB build-time (LLVM +
# toolchain). Runtime closure is only the two produced files —
# nothing emscripten-shaped survives into the deployed dist.
fluffychat-web-imaging = pkgs.stdenv.mkDerivation { fluffychat-web-imaging = pkgs.stdenv.mkDerivation {
pname = "fluffychat-web-imaging"; pname = "fluffychat-web-imaging";
version = pkgs.fluffychat-web.passthru.pubspecLock.dependencyVersions.native_imaging; version = pkgs.fluffychat-web.passthru.pubspecLock.dependencyVersions.native_imaging;
@ -72,10 +102,18 @@ let
}; };
}; };
# `pkgs.fluffychat-web` with #685's three missing files patched in # `pkgs.fluffychat-web` with #685's three missing files patched
# via postInstall. Mount point is `matrix.<hive>/` (#772); upstream # in via postInstall, plus the existing `--base-href "/matrix/"`
# `--base-href "/"` is correct at sub-domain root, no override. # override (#634) for the sub-path mount.
fluffychat-web-fixed = pkgs.fluffychat-web.overrideAttrs (old: { fluffychat-web-fixed = pkgs.fluffychat-web.overrideAttrs (old: {
# `--base-href "/matrix/"` so relative asset paths resolve
# under the sub-path mount (#634). Upstream default is `/`,
# wrong for hyperhive's `/matrix/` location.
flutterBuildFlags = (old.flutterBuildFlags or [ ]) ++ [
"--base-href"
"/matrix/"
];
# `dart` from the flutter341 closure (already pulled, no # `dart` from the flutter341 closure (already pulled, no
# incremental closure cost) so we can compile the web-worker # incremental closure cost) so we can compile the web-worker
# entry point that buildFlutterApplication skips. # entry point that buildFlutterApplication skips.
@ -84,14 +122,34 @@ let
postInstall = postInstall =
(old.postInstall or "") (old.postInstall or "")
+ '' + ''
# `web/...` is relative to build CWD so dart's package_config # #685: compile web/native_executor.dart → native_executor.js.
# walk-up hits buildFlutterApplication's pub-get output (#685 # The flutter web bootstrap loads this from /matrix/native_executor.js
# / #733 fixup — `$src/web/...` would walk up to a read-only # at startup; without it, main.dart.js logs a network-error and
# store path with no `.dart_tool/`). # the SPA renders blank (see #643 for the symptom).
#
# `dart compile js` needs `.dart_tool/package_config.json` to
# resolve `package:matrix/...` and the rest of fluffychat's
# `pubspec.lock` deps. buildFlutterApplication's pub-get step
# writes that file to the build CWD (the unpacked source dir),
# not to `$src` (the read-only nix store path). So we must use
# a relative path that walks up from `web/` to the build CWD
# where pub-get's package_config lives — pointing at
# `$src/web/native_executor.dart` walks up to `$src/`, finds
# no `.dart_tool/`, and fails with `Couldn't resolve the
# package 'matrix'` (mara's first build attempt on #685).
#
# nixpkgs's buildFlutterApplication leaves CWD at the source
# root for postInstall (see `pkgs/development/compilers/flutter/
# build-support/build-flutter-application.nix` — installPhase
# is `cp -r build/web "$out"` with no `cd` first). So `web/...`
# resolves correctly here.
${pkgs.flutter341.dart}/bin/dart compile js \ ${pkgs.flutter341.dart}/bin/dart compile js \
-o $out/native_executor.js \ -o $out/native_executor.js \
web/native_executor.dart web/native_executor.dart
# #685: install Imaging.{js,wasm} built from the native_imaging
# dart package's C source via emscripten (see
# `fluffychat-web-imaging` above for the build-time rationale).
install -m 644 ${fluffychat-web-imaging}/Imaging.js $out/Imaging.js install -m 644 ${fluffychat-web-imaging}/Imaging.js $out/Imaging.js
install -m 644 ${fluffychat-web-imaging}/Imaging.wasm $out/Imaging.wasm install -m 644 ${fluffychat-web-imaging}/Imaging.wasm $out/Imaging.wasm
''; '';
@ -205,17 +263,49 @@ in
''; '';
example = "matrix.example.com"; example = "matrix.example.com";
description = '' description = ''
Public hostname for the matrix homeserver behind the gateway. Public hostname for the matrix homeserver behind the
Defaults to `matrix.''${services.hyperhive.domain}` (sub-domain hive-gateway nginx (#747, mara verdict on #749:9609 — sub-domain
shape per mara on #749:9609). Set to `null` to skip the gateway over sub-path for matrix, but **not user-visible** because the
vhost (tuwunel stays direct on `httpPort`). See `.well-known/matrix/{client,server}` redirect routes clients
`docs/gateway.md` for the vhost map + matrix discovery flow, through automatically).
and the federation port-8448 caveat at the bottom of that doc.
Note: `gatewayHost` is the API listener hostname (where nginx When set + gateway is on, the gateway adds a `server { server_name
proxies `/_matrix/*`); `serverName` is the matrix-identifier = gatewayHost; }` block that proxies `/_matrix/...`
domain embedded irrevocably in user/room IDs (per #660 `http://127.0.0.1:''${httpPort}/_matrix/...`. The
default = bare hive-domain). The two are distinct. `.well-known/matrix/{client,server}` endpoints (served by the
gateway at the bare hive-domain) then point at
`http(s)://''${gatewayHost}/` matrix clients automatically
discover + follow that delegation.
Defaults to `matrix.''${services.hyperhive.domain}` when the
hive-domain is set (idiomatic matrix-spec shape `matrix`
labelled under the hive's bare server_name domain). Defaults to
`null` when the hive-domain is unset (gateway vhost not added;
clients reach tuwunel directly on `httpPort`).
Set to a full hostname (`matrix.example.com`,
`homeserver.internal.lan`) for a bespoke vhost shape. Set to
`null` to disable the gateway vhost entirely (tuwunel stays
direct on `httpPort`).
**server_name vs gatewayHost**: `serverName` is the matrix
identifier domain embedded in user/room IDs irrevocably (per
#660 default = bare hive-domain). `gatewayHost` is just where
the API listens behind nginx. The two are different see the
matrix-spec server-discovery flow.
**Federation port caveat**: the `.well-known/matrix/server`
delegation advertises `''${gatewayHost}` with no port suffix
when the gateway listens on 80. Per the matrix federation
spec, peers fall back to port 8448 when no explicit port is
present but the gateway only listens on the configured
`services.hyperhive.gateway.port`. Cross-hive federation
therefore needs either:
- a DNS SRV record (`_matrix._tcp.''${gatewayHost}` port 80),
- or `services.hyperhive.matrix.openFirewall = true` so peers
can reach tuwunel's federation port directly.
Hyperhive is mostly closed/internal, so this rarely bites in
practice but flagging for the federation-curious operator.
''; '';
}; };
@ -294,12 +384,22 @@ in
default = cfg.enable; default = cfg.enable;
defaultText = lib.literalExpression "config.services.hyperhive.matrix.enable"; defaultText = lib.literalExpression "config.services.hyperhive.matrix.enable";
description = '' description = ''
Serve a matrix web client at `matrix.''${services.hyperhive.domain}/`. Serve a matrix web client (default `pkgs.fluffychat-web`) as
Requires `gateway.enable` + `matrix.gatewayHost != null` a static dist at `/matrix/` via the hive-gateway nginx
(default true / `matrix.<hive>` when hive-domain set). When (#607 / #634). Defaults to whatever
off, the dashboard's `M4TR1X ` tab is hidden. See `services.hyperhive.matrix.enable` is turning on the
`docs/gateway.md` for the discovery flow that lets clients homeserver gives you the web client by default; set to
auto-find the sub-domain. `false` explicitly to opt out of the GUI while keeping
the homeserver running for agents. Requires
`services.hyperhive.gateway.enable` (default on); when
gateway is off no one hosts the GUI and the
`M4TR1X ` dashboard tab is hidden.
fluffychat-web supports per-login server pick point it at
the in-host tuwunel URL (`http://localhost:8008` by
default) the first time. The post-#15 nginx-front re-root
(`https://matrix.''${services.hyperhive.domain}`) is tracked
separately in #609.
''; '';
}; };
@ -307,15 +407,24 @@ in
type = lib.types.package; type = lib.types.package;
default = fluffychat-web-fixed; default = fluffychat-web-fixed;
defaultText = lib.literalMD '' defaultText = lib.literalMD ''
`pkgs.fluffychat-web` + #685 `postInstall` patch (adds the `pkgs.fluffychat-web` rebuilt with `--base-href /matrix/` (#634)
three files `flutter341.buildFlutterApplication` skips). and patched via `postInstall` to add the three files
`flutter341.buildFlutterApplication` skips: `native_executor.js`
(compiled via `dart compile js` from `web/native_executor.dart`),
plus `Imaging.js` + `Imaging.wasm` (built from the
`native_imaging` dart package's C source via `pkgs.emscripten`).
See the `let` block in `nix/modules/hive-matrix.nix` for the
full rationale (#685).
''; '';
description = '' description = ''
Static web client dist served at `matrix.<hive>/`. Override Static web client dist to serve at `/matrix/`. Defaults to
to swap fluffychat for hydrogen-web, cinny, element-web, or `pkgs.fluffychat-web` rebuilt with `--base-href "/matrix/"`
an out-of-tree dist any replacement is mounted at the so relative asset paths resolve under the sub-path mount
sub-domain root with the upstream-default `<base href "/">`, (#634), plus a `postInstall` patch for #685's three missing
no sub-path gymnastics needed. files. Override to swap for `hydrogen-web` (lightest),
`cinny` (no threads), `element-web` (heaviest, full
features), or an out-of-tree client dist any replacement
also needs its `<base href>` aligned with the mount path.
''; '';
}; };
}; };