diff --git a/Docs/Plannen/Actief/008-Umbrelapp/PROGRESS.md b/Docs/Plannen/Actief/008-Umbrelapp/PROGRESS.md index 1b7010a..9ae0704 100644 --- a/Docs/Plannen/Actief/008-Umbrelapp/PROGRESS.md +++ b/Docs/Plannen/Actief/008-Umbrelapp/PROGRESS.md @@ -3,6 +3,23 @@ > Chronologisch sessielog, nieuwste bovenaan. Kort: 3 tot 6 regels per entry. Wat er is gebeurd en waarom, > niet wat er nog moet: dat staat in [TAKEN.md](TAKEN.md). +## 28-08-2026 (avond) - het pakket is omgebouwd: relay, agent en pagina + +De app-map is nu de kale relay met onze allowlist eromheen. De poorten zijn omgedraaid: de statuspagina +hangt achter `app_proxy` mét de inlog van umbrelOS, de relay publiceert 3852 waar Zoraxy met TLS naartoe +gaat wijzen. Postgres, het wachtwoord en de quota-manager zijn eruit. + +De gebruiker heeft de image gebouwd en naar het Gitea-register geduwd; de compose is gepind op tag plus +digest. Manifest naar 0.2.0, met een beschrijving en releaseNotes die niet langer over Trezor's relay en +een quota-manager gaan. + +**Alles van vanavond is ongetest gedrag.** De agent is alleen op syntaxis gecontroleerd, de pagina is nooit +gerenderd, en of een opdracht van de pagina bij het relay-proces aankomt is niet gemeten. Dat is de +eerstvolgende taak en die kan alleen op het apparaat. + +**Geraakt:** de hele app-map, `tools/evolu-relay/build.sh` en de documentatie van dit plan. +**Tests:** alle vier groen (32, 54, 39 en 60 goed, 0 fout). + ## 28-08-2026 (later) - het relay-programma staat, met een test erop `tools/evolu-relay/src/` bevat nu een eigen programma dat `createRelay` uit `@evolu/nodejs` aanroept met diff --git a/Docs/Plannen/Actief/008-Umbrelapp/TAKEN.md b/Docs/Plannen/Actief/008-Umbrelapp/TAKEN.md index fb95b92..875a3af 100644 --- a/Docs/Plannen/Actief/008-Umbrelapp/TAKEN.md +++ b/Docs/Plannen/Actief/008-Umbrelapp/TAKEN.md @@ -63,13 +63,17 @@ eromheen, want de compose hangt af van wat dat programma nodig heeft. - [x] **`tools/evolu-relay/build.sh` omgeschreven (28-08-2026):** bouwt de eigen `Dockerfile` in plaats van de repo van Trezor te klonen. Git is niet meer nodig, de pin zit nu in `package.json`, en `VERSION` in het script hoort gelijk te zijn aan `version` in het manifest -- [ ] **De compose omzetten:** pagina achter `app_proxy` met de inlog aan, relay op een eigen `ports:`. - Postgres, wachtwoord en quota-manager eruit. Zie [PLAN.md](PLAN.md) §4h -- [ ] **De statuspagina bouwen**, met de pagina en de agent van Electrum Gate als vertrekpunt en met - hetzelfde ontwerpsysteem (`HomeGit/Docs/website-design-system.html`), maar zonder de Google - Fonts-verwijzing daaruit. Toont of de relay draait, welke eigenaars bekend en toegelaten zijn, de - omvang van de database en het laatste schrijfmoment. Draagt de schakelaar voor nieuwe eigenaars en de - wisknop per eigenaar +- [x] **De compose omgezet (28-08-2026):** pagina achter `app_proxy` mét de inlog, relay op host-poort + 3852. Postgres, wachtwoord en quota-manager eruit; drie containers werden relay, agent en nginx. De + image is gepind op tag plus digest +- [x] **De statuspagina gebouwd (28-08-2026):** `index.html.template`, `nginx.conf.template` en + `agent.py.template`, met het ontwerpsysteem van Electrum Gate en zonder de Google Fonts-verwijzing. + Toont of de relay draait, de omvang en het laatste schrijfmoment van de database, het adres dat je in + Suite invult, en de eigenaars in drie lijsten met knoppen om te blokkeren, alsnog toe te laten of te + vergeten. **Nog nooit in een browser gezien** +- [ ] **De pagina en de agent op het apparaat verifiëren.** Alles hierboven is ongetest gedrag: de agent is + alleen op syntaxis gecontroleerd, de pagina is nooit gerenderd, en of de opdrachten daadwerkelijk bij + het relay-proces aankomen is niet gemeten. **Eigenaar: gebruiker** - [ ] **De app opnieuw installeren in plaats van updaten.** De gebruiker heeft de oude installatie op 28-08-2026 weggehaald; dat is ook de nette weg, want niet elk bestand bereikt een bestaande installatie via een update diff --git a/whatsnext-evolu-relay/agent.py.template b/whatsnext-evolu-relay/agent.py.template new file mode 100644 index 0000000..0d2d67b --- /dev/null +++ b/whatsnext-evolu-relay/agent.py.template @@ -0,0 +1,259 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# De agent van Evolu Relay. +# +# Hij bedient de statuspagina en doet verder niets: lezen wat het relay-proces +# heeft opgeschreven, en opdrachten van de pagina in de postbus leggen. +# +# Waarom de agent niet zelf beslist wie er binnen mag: dat beleid hoort bij het +# proces dat de verbindingen aanneemt, en dat is de relay. Twee processen die in +# dezelfde allowlist schrijven is een wedloop die je een keer per jaar treft en +# dan niet kunt reproduceren. De agent schrijft daarom uitsluitend command.json, +# en het relay-proces past hem toe en ruimt hem op. +# +# LET OP: umbreld haalt dit bestand bij elke start door envsubst. Er mag dus geen +# dollarteken in staan, ook niet in een regex of een tekst. Een accolade-variabele +# die niet bestaat wordt leeg, en dat sloopt Python-code zonder foutmelding. +# tests/test_appstore_vorm.py controleert dat. +# ═══════════════════════════════════════════════════════════════════════════════ + +import json +import os +import socket +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +STATE_DIR = Path(os.environ.get("RELAY_STATE_DIR", "/var/lib/relay")) +OWNERS_FILE = STATE_DIR / "owners.json" +COMMAND_FILE = STATE_DIR / "command.json" +DATABASE_FILE = STATE_DIR / "evolu-relay.db" + +RELAY_HOST = os.environ.get("RELAY_HOST", "") +RELAY_PORT = int(os.environ.get("RELAY_PORT", "4000")) +PUBLIC_PORT = int(os.environ.get("RELAY_PUBLIC_PORT", "3852")) +API_PORT = int(os.environ.get("RELAY_API_PORT", "8000")) +PROBE_INTERVAL = int(os.environ.get("RELAY_PROBE_INTERVAL", "15")) + +# Wat de pagina mag vragen. Expliciet en niet doorgeven wat er binnenkomt: dit +# bestand wordt door een ander proces uitgevoerd, en een onbekende actie hoort +# hier te stranden en niet daar. +ALLOWED_ACTIONS = ("set-learning", "block", "allow", "forget") + +MAX_BODY_BYTES = 4096 +MAX_OWNER_ID_LENGTH = 256 + +# Door de achtergrondlus bijgewerkt, door de webserver gelezen. Een dict wordt in +# zijn geheel vervangen en nooit ter plekke aangepast, zodat een lezer altijd een +# samenhangend beeld heeft zonder slot. +probe = {"reachable": None, "checked": None} + + +def relay_reachable(): + """Kan de relay een TCP-verbinding aannemen. + + Bewust niet meer dan dat. De relay is een WebSocket-server en antwoordt niet + op een gewoon verzoek; een handdruk nabouwen om de pagina groen te krijgen is + meer code dan het waard is. Wat dit wel uitsluit is de meest voorkomende + storing: het proces is omgevallen. + """ + if not RELAY_HOST: + return None + try: + with socket.create_connection((RELAY_HOST, RELAY_PORT), timeout=3): + return True + except OSError: + return False + + +def probe_loop(): + global probe + while True: + # In zijn geheel vervangen en niet ter plekke aanpassen: een lezer ziet + # dan altijd een samenhangend beeld, zonder dat er een slot nodig is. + probe = { + "reachable": relay_reachable(), + "checked": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + } + time.sleep(PROBE_INTERVAL) + + +def read_owners(): + """De allowlist zoals het relay-proces hem heeft achtergelaten.""" + try: + with OWNERS_FILE.open("r", encoding="utf-8") as handle: + data = json.load(handle) + except FileNotFoundError: + # Nog nooit geschreven. Dat is de normale toestand vlak na een + # installatie: het relay-proces schrijft pas bij de eerste wijziging. + return {"state": None, "problem": "nog-niet-aangemaakt"} + except (OSError, ValueError): + return {"state": None, "problem": "onleesbaar"} + + if not isinstance(data, dict): + return {"state": None, "problem": "onleesbaar"} + return {"state": data, "problem": None} + + +def database_facts(): + try: + stat = DATABASE_FILE.stat() + except OSError: + return {"bytes": None, "modified": None} + return { + "bytes": stat.st_size, + "modified": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(stat.st_mtime)), + } + + +def build_status(): + owners = read_owners() + state = owners["state"] or {} + return { + "relay": { + "reachable": probe["reachable"], + "checked": probe["checked"], + "publicPort": PUBLIC_PORT, + }, + "owners": { + "problem": owners["problem"], + # Ontbreekt de staat, dan is 'learning' onbekend en niet 'false'. De + # pagina hoort dat verschil te tonen: onbekend is een reden om te + # kijken, uit is een keuze. + "learning": state.get("learning"), + "allowed": [ + entry + for entry in state.get("owners", []) + if isinstance(entry, dict) and entry.get("allowed") is True + ], + "blocked": [ + entry + for entry in state.get("owners", []) + if isinstance(entry, dict) and entry.get("allowed") is False + ], + "rejected": [ + entry for entry in state.get("rejected", []) if isinstance(entry, dict) + ], + }, + "database": database_facts(), + # Ligt er nog een opdracht, dan heeft het relay-proces hem nog niet + # opgepakt. De pagina kan dat tonen in plaats van te doen alsof er niets + # gebeurd is. + "pendingCommand": COMMAND_FILE.exists(), + } + + +def valid_command(payload): + """Geeft de opdracht terug, of een foutmelding. + + Streng aan deze kant, want dit is de enige plek waar iets van buiten in de + postbus belandt. + """ + if not isinstance(payload, dict): + return None, "geen object" + + action = payload.get("action") + if action not in ALLOWED_ACTIONS: + return None, "onbekende actie" + + if action == "set-learning": + value = payload.get("value") + if not isinstance(value, bool): + return None, "waarde moet true of false zijn" + return {"action": action, "value": value}, None + + owner_id = payload.get("ownerId") + if not isinstance(owner_id, str) or not owner_id or len(owner_id) > MAX_OWNER_ID_LENGTH: + return None, "ontbrekende of te lange ownerId" + return {"action": action, "ownerId": owner_id}, None + + +def write_command(command): + """Legt de opdracht in de postbus. + + Eerst een tijdelijk bestand en dan hernoemen: het relay-proces kijkt op zijn + eigen moment en mag geen half bestand aantreffen. + """ + STATE_DIR.mkdir(parents=True, exist_ok=True) + temporary = COMMAND_FILE.with_suffix(".json.tmp") + with temporary.open("w", encoding="utf-8") as handle: + json.dump(command, handle) + handle.write("\n") + temporary.replace(COMMAND_FILE) + + +class Handler(BaseHTTPRequestHandler): + # De standaardregel van BaseHTTPRequestHandler noemt de naam van de server en + # de Python-versie. Dat hoeft niemand te weten. + server_version = "evolu-relay-agent" + sys_version = "" + + def log_message(self, format, *args): + # Geen toegangslog. Elke regel zou het adres van de bezoeker bevatten en + # de pagina zit achter de inlog van umbrelOS; er valt niets te zien wat + # het bewaren waard is. + return + + def _send(self, status, payload): + body = json.dumps(payload).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + if self.path.rstrip("/") in ("/api/status", "/status"): + self._send(200, build_status()) + return + self._send(404, {"error": "onbekend pad"}) + + def do_POST(self): + if self.path.rstrip("/") not in ("/api/command", "/command"): + self._send(404, {"error": "onbekend pad"}) + return + + try: + length = int(self.headers.get("Content-Length", "0")) + except ValueError: + self._send(400, {"error": "lengte ontbreekt"}) + return + + if length <= 0 or length > MAX_BODY_BYTES: + self._send(400, {"error": "lege of te grote opdracht"}) + return + + try: + payload = json.loads(self.rfile.read(length).decode("utf-8")) + except (UnicodeDecodeError, ValueError): + self._send(400, {"error": "onleesbare opdracht"}) + return + + command, problem = valid_command(payload) + if command is None: + self._send(400, {"error": problem}) + return + + # Eén opdracht tegelijk. Ligt er nog een, dan zou schrijven hem stil + # overschrijven en verdwijnt de vorige zonder dat iemand het merkt. + if COMMAND_FILE.exists(): + self._send(409, {"error": "vorige opdracht is nog niet verwerkt"}) + return + + try: + write_command(command) + except OSError as error: + self._send(500, {"error": "opdracht kon niet worden weggeschreven: " + str(error)}) + return + + self._send(202, {"accepted": command}) + + +def main(): + threading.Thread(target=probe_loop, daemon=True).start() + ThreadingHTTPServer(("0.0.0.0", API_PORT), Handler).serve_forever() + + +if __name__ == "__main__": + main() diff --git a/whatsnext-evolu-relay/data/postgres/.gitkeep b/whatsnext-evolu-relay/data/relay/.gitkeep similarity index 100% rename from whatsnext-evolu-relay/data/postgres/.gitkeep rename to whatsnext-evolu-relay/data/relay/.gitkeep diff --git a/whatsnext-evolu-relay/docker-compose.yml b/whatsnext-evolu-relay/docker-compose.yml index 5ed8a1d..322e0f0 100644 --- a/whatsnext-evolu-relay/docker-compose.yml +++ b/whatsnext-evolu-relay/docker-compose.yml @@ -1,135 +1,129 @@ # ═══════════════════════════════════════════════════════════════════════════════ -# Evolu Relay - de sync-server van Trezor Suite op je eigen Umbrel. +# Evolu Relay - je eigen sync-server voor de labels van Trezor Suite. # -# Drie containers, twee images. De relay en de quota-manager komen uit dezélfde -# image met een ander `command`: bovenstrooms is het één codebase met meerdere -# startscripts. Dat is nagetrokken in de Dockerfile en de README van Trezor; zie -# Docs/Referenties/Upstream-evolu-relay.md §2. +# Drie containers: de relay, een agent voor de statuspagina, en nginx die die +# pagina serveert. Geen Postgres en geen quota-manager meer; die hoorden bij de +# relay van Trezor, en die is als zelf-gehoste relay in de kern onbruikbaar omdat +# de cliënt bij een eigen relay-URL nooit een eigenaar registreert. Zie het plan +# Umbrelapp, OPEN.md punt 6, en de proef in PLAN.md §6a. +# +# De poortindeling is omgedraaid ten opzichte van 25-08-2026, en dat is de kern +# van dit bestand: +# +# - de PAGINA hangt achter app_proxy, mét de inlog van umbrelOS. Vroeger stond de +# relay daar en moest de inlog dus uit, waardoor een statuspagina net zo +# onbeschermd zou zijn als de relay zelf; +# - de RELAY publiceert zijn eigen poort, waar Zoraxy met TLS naartoe wijst. +# Trezor Suite is geen browser met een sessiecookie en zou achter de inlog een +# inlogpagina krijgen in plaats van de relay. +# +# Dat is hetzelfde patroon als Electrum Gate in deze store: de web-UI via de +# proxy, het protocol op een eigen poort. Zie PLAN.md §4h. # ═══════════════════════════════════════════════════════════════════════════════ services: + # umbrelOS genereert deze service zelf; wij vullen alleen in waar hij heen moet + # wijzen. De hostnaam heeft de vorm __1. + # + # PROXY_AUTH_ADD staat hier bewust NIET op "false". Dat stond er tot 0.0.2 wel, + # toen de relay hierachter hing. Nu wijst dit naar de statuspagina, en die hoort + # juist achter de inlog. Zet het niet terug zonder §4h te lezen. app_proxy: environment: - APP_HOST: whatsnext-evolu-relay_relay_1 - APP_PORT: 4000 - # Trezor Suite is geen browser met een sessiecookie. Achter de inlog van - # umbrelOS zou het een inlogpagina krijgen in plaats van de relay, en dat - # is niet op te lossen aan onze kant. - # - # Dit is geen omweg: het is het patroon dat de eigen nostr-relay-app van - # Umbrel gebruikt, om precies dezelfde reden. Ook Gitea en Budibase in de - # officiele store doen het zo. - # - # De prijs staat er wel bij: wie deze poort kan bereiken, bereikt de relay - # zonder aanmelding. Wat de schade beperkt is dat de relay zelf elke - # eigenaar weigert die geen limietenrij in de database heeft. Zet deze app - # dus niet zonder meer open naar het internet; zie het masterplan - # Bereikbaarheid. - # - # En let op de tweede weg naar binnen, die je niet ziet in deze compose: - # umbrelOS maakt per app een Tor hidden service. Zonder inlog betekent dat - # bereikbaar vanaf het internet, alleen beschermd doordat het .onion-adres - # onraadbaar is. Bij Electrum Gate is dat geen bezwaar, want daar zit de - # inlog van umbrelOS ervoor; hier is die er juist uit. Zet de hidden service - # dus uit in umbrelOS tenzij je hem bewust wilt. Op 25-08-2026 stond hij bij - # de eerste installatie gewoon aan. - PROXY_AUTH_ADD: "false" + APP_HOST: whatsnext-evolu-relay_server_1 + APP_PORT: 80 relay: - # Gebouwd uit de broncode van Trezor door tools/evolu-relay/build.sh, en - # geduwd naar het Gitea-register op dezelfde server als deze store. Dat moet: - # umbreld haalt élke image op via de Docker Engine API (docker-modem) en niet - # via compose, dus een lokaal gebouwde tag is voor hem onbereikbaar en - # `pull_policy` wordt nooit gelezen. Op 25-08-2026 op het apparaat - # vastgesteld; zie Docs/Referenties/Umbrel-appstore-spec.md. + # Gebouwd uit tools/evolu-relay/ door build.sh en geduwd naar het + # Gitea-register op dezelfde server als deze store. Dat moet: umbreld haalt + # élke image op via de Docker Engine API en niet via compose, dus een lokaal + # gebouwde tag is voor hem onbereikbaar en de installatie faalt met + # "pull access denied". Op 25-08-2026 op het apparaat vastgesteld. # # De digest hoort erbij en niet alleen de tag: een tag kan opnieuw geduwd - # worden en dan draait er iets anders dan hier staat. + # worden en dan draait er iets anders dan hier staat. Staan ze allebei, dan + # bepaalt de digest wat er gehaald wordt en is de tag alleen leesbaarheid. + # Houd de tag gelijk aan `version` in het manifest en aan VERSION in build.sh. # - # Let op: dit is de digest van één architectuur (amd64), want er is alleen - # amd64 geduwd. Voor de officiele store moet het een multi-arch index-digest - # zijn met arm64 erin; zie het masterplan Publicatie-Relay. - image: sc.kamenier-hamer.nl/sysop/evolu-relay:c03a204@sha256:2fe1e9e90e5cbc12d5ea6363df03bb132fddec0150283063b9103a09bdbe853a + # Geduwd op 28-08-2026. Let op: dit is één architectuur (amd64), want er is + # alleen amd64 gebouwd. Voor de officiele store hoort er een multi-arch + # index-digest met arm64 in; zie het masterplan Publicatie-Relay. + image: sc.kamenier-hamer.nl/sysop/evolu-relay:0.2.0@sha256:5b2b4228c24a7e8a12a99869ca39729f47a0a01d6a346c623b22d6e62f2918c2 restart: on-failure - depends_on: - db: - condition: service_healthy - # Bovenstrooms staat `CMD ["yarn", "start"]` in de Dockerfile, met daarbij een - # eigen commentaar dat het misschien een van de twee specifieke scripts had - # moeten zijn. Daarom hier expliciet, en niet vertrouwen op de standaard. - command: ["yarn", "start-evolu-relay"] + ports: + # De relay zelf. Dit is de poort waar Zoraxy met TLS naartoe wijst, en de + # enige die deze app publiceert; de pagina loopt via app_proxy. + # + # 3852 op de host en niet 4000: dat laatste is een veelgebruikte poort en + # een botsing merk je pas als de app niet start. Dezelfde les als bij + # Electrum Gate, dat om die reden 50022 gebruikt in plaats van 50002. + # Binnen de container blijft het 4000, want dat is wat de relay verwacht. + - "3852:4000" environment: RELAY_PORT: "4000" - HEALTH_PORT: "4002" - # dev of prod. Volgens .env.sample zet prod authenticatie aan; in de code - # bepaalt die vlag alleen het logniveau en staan de autorisatiecontroles - # onvoorwaardelijk aan. Een van de twee is achterhaald en dat is nog niet - # uitgezocht. Hier staat prod, want dat is de veilige kant van die - # onduidelijkheid: als het iets aanzet, willen we dat het aanstaat. - SERVER_ENV: prod - POSTGRES_GATE_HOST: whatsnext-evolu-relay_db_1 - POSTGRES_GATE_PORT: "5432" - POSTGRES_GATE_USER: suite-sync - POSTGRES_GATE_DB: suite-sync-gate - # Door umbrelOS per installatie afgeleid. Nooit een letterlijke waarde: - # deze repo is publiek. - POSTGRES_GATE_PASSWORD: ${APP_PASSWORD} - POSTGRES_GATE_SSL: "false" - - quota-manager: - # Zelfde image als relay, ander commando. Tag en digest moeten dus gelijk - # blijven aan die hierboven. - image: sc.kamenier-hamer.nl/sysop/evolu-relay:c03a204@sha256:2fe1e9e90e5cbc12d5ea6363df03bb132fddec0150283063b9103a09bdbe853a - restart: on-failure - depends_on: - db: - condition: service_healthy - command: ["yarn", "start-quota-manager"] - # Waarom deze container er is, terwijl hij bij Trezor bij hun betaalde - # hosting hoort: de relay weigert elke eigenaar zonder rij in de - # limietentabel, en dit is wat die rijen aanmaakt. Er is geen HTTP-koppeling - # tussen de twee; ze delen alleen de database. Zie Upstream-evolu-relay.md §3. - environment: - QUOTA_MANAGER_PORT: "4001" - HEALTH_PORT: "4012" - SERVER_ENV: prod - POSTGRES_GATE_HOST: whatsnext-evolu-relay_db_1 - POSTGRES_GATE_PORT: "5432" - POSTGRES_GATE_USER: suite-sync - POSTGRES_GATE_DB: suite-sync-gate - POSTGRES_GATE_PASSWORD: ${APP_PASSWORD} - POSTGRES_GATE_SSL: "false" - - db: - # TODO pinnen op de multi-arch index-digest, met - # `docker buildx imagetools inspect postgres:17-alpine`. Kan alleen op een - # machine met Docker; staat als taak in het plan Umbrelapp. - # - # Niet `postgres` kaal zoals de compose van Trezor doet: dat is `latest` en - # dus niet reproduceerbaar, en een grote-versiesprong van Postgres migreert - # zijn datamap niet vanzelf. Dan start de database niet meer en is de data - # alleen met handwerk terug te halen. - image: postgres:17-alpine - restart: on-failure - environment: - POSTGRES_USER: suite-sync - POSTGRES_DB: suite-sync-gate - POSTGRES_PASSWORD: ${APP_PASSWORD} - # Een submap en niet de wortel van de mount. Postgres weigert een datamap - # die al iets anders bevat, en een mount heeft op sommige bestandssystemen - # een lost+found. - PGDATA: /var/lib/postgresql/data/pgdata + # Per schrijfactie en niet per eigenaar; zo is isOwnerWithinQuota bij Evolu + # bedoeld. Eén labelwijziging is klein, dus een schrijfactie hierboven is + # eerder een fout of misbruik dan normaal gebruik. Zie PLAN.md §4g. + RELAY_MAX_WRITE_BYTES: "1048576" volumes: + # De database van de relay én onze allowlist ernaast, in dezelfde map. Het + # programma doet chdir naar `data`, net als de relay van Evolu zelf, dus dit + # pad ligt vast. + # # Onder data/ en niet in een naamloos Docker-volume: dit is wat umbrelOS - # bewaart en meeneemt in de back-up. In een volume zou de database een + # bewaart en meeneemt in de back-up. In een volume zou de synchronisatie een # herinstallatie van de app niet overleven. - - ${APP_DATA_DIR}/data/postgres:/var/lib/postgresql/data - healthcheck: - # Op de eigen database en gebruiker, niet op `-d postgres` zoals - # bovenstrooms: die database bestaat hier niet en dan is de controle groen - # terwijl het verkeerde antwoord gegeven wordt. - test: ["CMD-SHELL", "pg_isready -U suite-sync -d suite-sync-gate"] - interval: 10s - timeout: 5s - retries: 10 - start_period: 30s + - ${APP_DATA_DIR}/data/relay:/app/data + + # De agent bedient de statuspagina. Hij beslist niets: hij leest wat het + # relay-proces heeft opgeschreven en legt opdrachten in een postbus die de relay + # zelf leegmaakt. Twee processen die in dezelfde allowlist schrijven is een + # wedloop die je een keer per jaar treft en dan niet kunt reproduceren. + agent: + # TODO pinnen op de multi-arch index-digest, met + # `docker buildx imagetools inspect python:3-alpine`. Geldt ook voor nginx + # hieronder; staat als taak in het plan Umbrelapp. + image: python:3-alpine + restart: on-failure + environment: + RELAY_STATE_DIR: /var/lib/relay + # De volledige containernaam en niet de servicenaam 'relay'. Bij Electrum + # Gate gaf de korte naam op het apparaat een geweigerde verbinding: de naam + # loste wél op, maar naar een container van een andere app op hetzelfde + # netwerk. De vorm __1 is uniek per app. + RELAY_HOST: whatsnext-evolu-relay_relay_1 + RELAY_PORT: "4000" + # Alleen om op de pagina het adres te tonen dat je in Trezor Suite invult. + # Moet gelijk zijn aan de hostpoort hierboven. + RELAY_PUBLIC_PORT: "3852" + RELAY_API_PORT: "8000" + PYTHONUNBUFFERED: "1" + volumes: + # Door umbrelOS ingevuld uit agent.py.template bij het starten. Er staat + # geen accolade-variabele in dat bestand, dus de invulling laat de + # Python-code ongemoeid; de agent leest zijn instellingen uit de omgeving + # hierboven. De test in tests/ controleert die aanname. + - ${APP_DATA_DIR}/agent.py:/app/agent.py:ro + # Dezelfde map als de relay. De agent moet hier kunnen schrijven, want de + # opdrachten van de pagina landen hier. + - ${APP_DATA_DIR}/data/relay:/var/lib/relay + command: + - python + - /app/agent.py + + server: + image: nginx:alpine + restart: on-failure + # De agent moet er zijn voordat nginx start: de proxy_pass naar + # http://agent:8000 wordt bij het starten opgelost en een onbekende naam laat + # nginx afbreken. + depends_on: + - agent + volumes: + # Beide door umbrelOS ingevuld uit een .template bij het starten. + - ${APP_DATA_DIR}/nginx.conf:/etc/nginx/nginx.conf:ro + - ${APP_DATA_DIR}/index.html:/usr/share/nginx/html/index.html:ro + # Het app-icoon, voor de kop van de pagina en het tabblad. Geen template, + # dus dit bestand komt alleen bij een installatie mee en niet bij een + # update; voor een plaatje dat vrijwel nooit wijzigt is dat goed genoeg. + - ${APP_DATA_DIR}/icon.png:/usr/share/nginx/html/icon.png:ro diff --git a/whatsnext-evolu-relay/index.html.template b/whatsnext-evolu-relay/index.html.template new file mode 100644 index 0000000..62174c2 --- /dev/null +++ b/whatsnext-evolu-relay/index.html.template @@ -0,0 +1,480 @@ + + + + + + +Evolu Relay + + + + +
+ +
+ +

Evolu Relay

+ + +
+

Your own sync server for Trezor Suite labels. Nothing leaves your machine.

+ +
+ +
+

Relay

+
+
+
Status
+
checking
+
+
+
Stored data
+
-
+
+
+
Last write
+
-
+
+
+
+ +
+

Point Trezor Suite here

+
-
+

+ In Trezor Suite, open the developer settings and set the custom relay URL. Leave the quota manager + URL empty: Suite ignores it once a custom relay is set. The address must start with http or https; + Suite rejects ws. +

+
+ +
+

New owners

+
+
+
unknown
+

+ While this is open, the next device that connects is accepted and remembered. Close it once your + own devices are paired. +

+
+ +
+
+ +
+

Accepted owners

+

None yet.

+
+ +
+

Blocked owners

+

None.

+
+ +
+

Refused attempts

+

None.

+

+ Devices that tried to connect while new owners were closed. Only the twenty most recent are kept. +

+
+ +
Evolu Relay on umbrelOS · WhatsNext?
+
+ + + + diff --git a/whatsnext-evolu-relay/nginx.conf.template b/whatsnext-evolu-relay/nginx.conf.template new file mode 100644 index 0000000..bce0cd9 --- /dev/null +++ b/whatsnext-evolu-relay/nginx.conf.template @@ -0,0 +1,58 @@ +# nginx-configuratie voor Evolu Relay. +# +# Dit bestand serveert alleen de statuspagina. De relay zelf zit er niet achter: +# die publiceert zijn eigen poort in docker-compose.yml, waar Zoraxy met TLS +# naartoe wijst. Dat is de omkering ten opzichte van het pakket van 25-08-2026, +# waar de relay achter de app-proxy stond en de inlog dus uit moest; zie het plan +# Umbrelapp, PLAN.md §4h. +# +# Dit is een template, en dat is bewust: umbrelOS ververst bij een update alleen +# een whitelist van bestanden, en *.template staat daarin. Bij elke start vult +# umbrelOS de omgevingsvariabelen in en schrijft het resultaat weg als nginx.conf. +# +# Let op bij het wijzigen: gebruik hier geen nginx-variabelen met een dollarteken, +# zoals de gebruikelijke in een log_format. De invulling vervangt elke +# accolade-variabele en zou die stilzwijgend leegmaken. Daarom staat access_log +# uit. + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log off; + sendfile on; + + server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + # De API van de agent: de status lezen en een opdracht in de postbus + # leggen. Dit is het enige pad waarlangs iets van buiten de instellingen + # van de app raakt. + # + # Waarom dat hier te verantwoorden is: dit pad hangt achter de app_proxy + # van umbrelOS, die er zijn eigen inlog voor zet. De relay staat op een + # andere poort en is hierlangs niet te bereiken. En het ergste wat een + # geslaagde aanroep doet is de leerstand omzetten of een eigenaar + # blokkeren, en dat is precies waar de pagina voor is. + # + # Geen proxy_set_header, en dat is geen vergetelheid: die zouden een + # nginx-variabele vragen en die haalt de template-invulling weg. De agent + # heeft ze niet nodig. + location /api/ { + proxy_pass http://agent:8000; + # Een opdracht is een handvol bytes. De agent kapt zelf ook af, op + # 4k; dit is de eerste zeef en niet de enige. + client_max_body_size 4k; + } + } +} diff --git a/whatsnext-evolu-relay/umbrel-app.yml b/whatsnext-evolu-relay/umbrel-app.yml index 5d7a895..329d891 100644 --- a/whatsnext-evolu-relay/umbrel-app.yml +++ b/whatsnext-evolu-relay/umbrel-app.yml @@ -11,7 +11,7 @@ manifestVersion: 1 id: whatsnext-evolu-relay category: bitcoin name: Evolu Relay -version: "0.0.2" +version: "0.2.0" tagline: Sync your Trezor Suite labels through your own machine description: >- Trezor Suite can sync the labels and account names you give your addresses across your @@ -24,43 +24,47 @@ description: >- can see that you are syncing at all. - This is Trezor's own relay, not a reimplementation: it is built from the source they publish - as trezor/trezor-suite-sync, pinned to a specific commit. + This runs the relay from the Evolu project, the sync layer Trezor Suite is built on, taken + straight from the package they publish. Nothing about it is reimplemented here. What this app + adds is the part a self-hosted relay needs and an open one does not: control over who may use + it. + + + The tile opens a status page. It shows whether the relay is running, how much it stores, and + which devices are allowed. New devices are accepted one at a time: the first owner that + connects is remembered, and you close the door again once your own devices are paired. Any + owner can be blocked or removed later from that same page. Point Trezor Suite at this Umbrel to use it. Away from home you will need a way in, such as Tailscale or a reverse proxy with your own domain. - - - Two things to know before you install. The relay has no web interface, so the tile opens the - relay itself rather than a page for you to read. And it only serves an owner that has a - storage limit registered in its database, which is what the included quota manager is for; - if syncing is refused, that registration is the place to look. releaseNotes: >- - First release that can actually be installed. 0.0.1 pointed at an image that only existed - on the machine it was built on, and the install failed before anything started: umbrel - fetches every image from a registry, so a local build is out of reach. The image now comes - from a registry and is pinned to its digest. + A different relay, and a much smaller app. 0.0.2 packaged the relay Trezor runs for their own + hosted service, with a quota manager and a PostgreSQL database beside it. That combination + cannot work on your own machine: Suite skips the quota manager as soon as you point it at a + relay of your own, while that relay refuses every owner the quota manager never registered. - Packages Trezor's Evolu Relay, the quota manager it needs, and a PostgreSQL database, all on - your own machine. + This release uses the relay from the Evolu project instead, which Trezor Suite talks to + directly. Measured, not assumed: Suite sent labels to it and they arrived. The database and + the quota manager are gone, and three containers became one relay plus a status page. - Built from source pinned to commit c03a204 of trezor/trezor-suite-sync. There is no image - published by Trezor, so the image is built from their Dockerfile; see tools/evolu-relay in - the app store repository. + New: a status page behind your umbrelOS login, and control over who may sync. The first owner + that connects is remembered and accepted; after that you close the door, and anything new is + refused until you open it again. - Not verified yet: whether Trezor Suite accepts this address, and what has to happen before - an owner is allowed to sync. + Not verified yet: reading back from a second device, and whether the iOS app can use a relay + of your own at all. developer: "WhatsNext?" website: https://sc.kamenier-hamer.nl/sysop/UmbrelApps repo: https://sc.kamenier-hamer.nl/sysop/UmbrelApps support: https://sc.kamenier-hamer.nl/sysop/UmbrelApps/issues -# De poort waarop umbrelOS deze app aanbiedt, en tegelijk de poort waar Trezor -# Suite naartoe wijst: de app-proxy staat met PROXY_AUTH_ADD op "false", dus wat -# hier binnenkomt gaat rechtstreeks naar de relay. Zie docker-compose.yml. +# De poort waarop umbrelOS deze app aanbiedt. Sinds 0.1.0 is dat de STATUSPAGINA +# en niet de relay: de app-proxy zet er de inlog van umbrelOS voor, en daar hoort +# een pagina wel achter en een relay niet. Trezor Suite wijst naar 3852, die de +# relay zelf publiceert. Zie docker-compose.yml en het plan Umbrelapp, PLAN.md §4h. # # Niet 4000, de eigen poort van de relay: die is een veelgebruikte poort en een # botsing op de host merk je pas als de app niet start. Dat heeft bij Electrum