Compare commits
33 Commits
beta
...
1ef32c0022
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ef32c0022 | |||
| 66bf54bdff | |||
| 81ec06398f | |||
| 4477ed9101 | |||
| 825481e6fa | |||
| db4d001ef1 | |||
| af7c538e18 | |||
| 4c6efa2a7a | |||
| d3e75c36a1 | |||
| 326cc598c7 | |||
| 13b333cbec | |||
| 4d4044b888 | |||
| 5ada0e5038 | |||
| c8ff850c98 | |||
| d42beaf7ab | |||
| 343545b59e | |||
| 455742d1bf | |||
| e97a748b35 | |||
| ba97f942d6 | |||
| 88af6c016c | |||
| 78e4174658 | |||
| 925aafb2fe | |||
| fe6427bfba | |||
| a5a5c4baf5 | |||
| 7b90f20e36 | |||
| 03b84e0e0e | |||
| 7dfdbf1159 | |||
| e0ac5cb9fd | |||
| eebf8ae7dd | |||
| 566938f004 | |||
| 1630d0e389 | |||
| 99cde9a1e2 | |||
| bfe58760ee |
@@ -0,0 +1,42 @@
|
|||||||
|
# Bernds Entscheidungen – Richtungsfragen aus PROGRESS.md beantwortet
|
||||||
|
|
||||||
|
Diese Datei beantwortet die über mehrere Sessions in PROGRESS.md gesammelten
|
||||||
|
„Fragen an Bernd". Sie ist ab jetzt **maßgeblich** – die betroffenen Punkte in
|
||||||
|
ROADMAP.md/PROGRESS.md gelten als entschieden.
|
||||||
|
Zuletzt aktualisiert: **2026-07-05** (Bernd + Claude am PC).
|
||||||
|
|
||||||
|
## 1. SQLCipher (lokale DB + Access-Token verschlüsseln) → VOR M2 priorisieren
|
||||||
|
- **Entscheidung:** Ja. Die Verschlüsselung der lokalen `pyramid.sqlite` hat Vorrang
|
||||||
|
vor dem M2-Refactoring. Betrifft den M1-Punkt „Sichere Speicherung von Access-Token
|
||||||
|
& Krypto-DB".
|
||||||
|
- Umsetzung nach `docs/SQLCIPHER_MIGRATION.md`. **Wichtig:** Die Migration bestehender
|
||||||
|
Klartext-DBs (Utas Gerät!) ist der riskante Teil und braucht einen echten
|
||||||
|
PC-/Gerätetest. Auf dem Pi (kein GUI) nur so weit vorbereiten, wie es OHNE GUI sicher
|
||||||
|
testbar ist; alles Verhaltensrelevante klar als UNGETESTET markieren und erst nach
|
||||||
|
`docs/PC_TESTPLAN.md` als erledigt abhaken. Grundsatz „Kein Datenverlust" – im Zweifel
|
||||||
|
für den PC-Termin liegen lassen.
|
||||||
|
|
||||||
|
## 2. M2 – erstes Umbau-Modul → Call-Pilot zuerst
|
||||||
|
- **Entscheidung:** Wenn M2 startet (nach SQLCipher), zuerst die Call-Schicht entwirren
|
||||||
|
(Call-Pilot / gemeinsame Fassade über `voip_manager.dart` + `livekit_call_manager.dart`),
|
||||||
|
wie in `docs/M2_MODULE_SCHNITT.md` vorgeschlagen. **Nicht** zuerst `settings_modal.dart`
|
||||||
|
aufsplitten.
|
||||||
|
- **Wichtig:** heiliger Call-Pfad → braucht zwingend echten Call-Test auf einem Gerät.
|
||||||
|
Auf dem Pi nicht blind umbauen; Vorbereitung/Analyse ok, Umsetzung + Test am PC.
|
||||||
|
|
||||||
|
## 3. Dashboard-Fernzugriff (stats.html) → Heimnetz-only lassen
|
||||||
|
- **Entscheidung:** KEIN Fernzugriff. Das Interims-Gate (nur Heimnetz/localhost) bleibt so.
|
||||||
|
Die offene A/B-Frage (Cloudflare Access vs. DASH_TOKEN) aus
|
||||||
|
`docs/DASHBOARD_AUTH_HARDENING.md` ist damit **geschlossen** – nicht weiter daran
|
||||||
|
arbeiten. Unterwegs per WireGuard erreichbar.
|
||||||
|
|
||||||
|
## 4. Git-History / Admin-Token → vorerst NICHTS tun
|
||||||
|
- **Entscheidung:** Kein Git-History-Rewrite und aktuell keine Token-Rotation durch den
|
||||||
|
Autopilot. Der Admin-Token ist bereits aus dem App-Code heraus (eigener `DIAG_TOKEN`).
|
||||||
|
Eine spätere Rotation bleibt Bernds manuelle Sache – nicht eigenmächtig anstoßen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Kurzfassung fürs Arbeiten:** SQLCipher vor M2, dann Call-Pilot als erstes M2-Modul.
|
||||||
|
Dashboard bleibt Heimnetz-only. History unangetastet. Riskante/GUI-relevante Teile
|
||||||
|
warten auf den PC-Termin (`docs/PC_TESTPLAN.md`); auf dem Pi nur sicher Vorbereitbares.
|
||||||
+106
@@ -1199,3 +1199,109 @@
|
|||||||
2026-07-03 17:51 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid_push.md
|
2026-07-03 17:51 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid_push.md
|
||||||
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/MEMORY.md
|
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/MEMORY.md
|
||||||
|
2026-07-03 18:16 [Write] /home/steggi/pyramid/test/soft_logout_guard_test.dart
|
||||||
|
2026-07-03 18:17 [Write] /home/steggi/pyramid/test/power_levels_safety_test.dart
|
||||||
|
2026-07-03 22:21 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 22:22 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 22:23 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-03 22:23 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
|
||||||
|
2026-07-03 22:48 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 22:50 [Write] /home/steggi/pyramid/test/auth_log_test.dart
|
||||||
|
2026-07-03 22:50 [Edit] /home/steggi/pyramid/pubspec.yaml
|
||||||
|
2026-07-03 22:51 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 22:52 [Write] /home/steggi/pyramid/test/settings_prefs_test.dart
|
||||||
|
2026-07-03 22:53 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 22:53 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
|
||||||
|
2026-07-03 22:54 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-03 23:18 [Edit] /home/steggi/pyramid/test/settings_prefs_test.dart
|
||||||
|
2026-07-03 23:18 [Edit] /home/steggi/pyramid/test/settings_prefs_test.dart
|
||||||
|
2026-07-03 23:19 [Edit] /home/steggi/pyramid/lib/core/settings_prefs.dart
|
||||||
|
2026-07-03 23:20 [Edit] /home/steggi/pyramid/lib/core/settings_prefs.dart
|
||||||
|
2026-07-03 23:21 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 23:21 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
|
||||||
|
2026-07-03 23:22 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 03:28 [Edit] /home/steggi/pyramid/scripts/release.ps1
|
||||||
|
2026-07-04 03:31 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 03:32 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 03:32 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 04:01 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 04:02 [Edit] /home/steggi/pyramid/lib/widgets/settings/settings_encryption.dart
|
||||||
|
2026-07-04 04:03 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 04:03 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 04:06 [Write] /home/steggi/pyramid/docs/LIVEKIT_TOKEN_MIGRATION.md
|
||||||
|
2026-07-04 04:06 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 04:06 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 04:07 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-04 04:30 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 04:30 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 04:31 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 04:32 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 08:42 [Write] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||||
|
2026-07-04 08:42 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 08:43 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 08:43 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 09:14 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 09:14 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 09:15 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||||
|
2026-07-04 09:15 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||||
|
2026-07-04 09:16 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 09:16 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 09:17 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 09:17 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 09:18 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-04 09:43 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 09:43 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 09:43 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 13:46 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 13:46 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 13:50 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 13:51 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 13:51 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 13:51 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-04 14:17 [Write] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 14:19 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 14:20 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 14:20 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 14:43 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 14:45 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 14:46 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 14:46 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 14:47 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 18:56 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 18:56 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 18:56 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 18:57 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-04 19:19 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 19:20 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 19:21 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 19:21 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 19:22 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-04 19:44 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 19:44 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 19:44 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 19:45 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-05 14:51 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-05 14:52 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-05 14:52 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-05 14:52 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-05 14:52 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-05 14:52 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||||
|
2026-07-05 14:52 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||||
|
2026-07-05 14:52 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||||
|
2026-07-05 14:53 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||||
|
2026-07-05 14:53 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-05 14:54 [Write] /home/steggi/.claude/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand-haupt.md
|
||||||
|
2026-07-05 14:54 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-05 14:54 [Edit] /home/steggi/.claude/projects/-home-steggi-pyramid/memory/MEMORY.md
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ Einsatz (u. a. Uta) – Stabilität und Datensicherheit gehen vor Feature-Tempo.
|
|||||||
(`C:\Users\nordm\pyramid` und `C:\Users\nordm\MatrixPi\pyramid` sind veraltete
|
(`C:\Users\nordm\pyramid` und `C:\Users\nordm\MatrixPi\pyramid` sind veraltete
|
||||||
April-Schnappschüsse – dort NIE arbeiten.)
|
April-Schnappschüsse – dort NIE arbeiten.)
|
||||||
|
|
||||||
|
> **Bernds Entscheidungen zu den offenen Richtungsfragen stehen in `ANTWORTEN_BERND.md`
|
||||||
|
> (zuletzt 2026-07-05) – vor dem Abarbeiten von ROADMAP-Punkten lesen, sie sind maßgeblich.**
|
||||||
|
|
||||||
## Infrastruktur
|
## Infrastruktur
|
||||||
|
|
||||||
- Homeserver: Continuwuity auf dem Pi5 (`steggi-matrix.work`)
|
- Homeserver: Continuwuity auf dem Pi5 (`steggi-matrix.work`)
|
||||||
|
|||||||
+960
@@ -13,6 +13,744 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-05 – Dashboard-ROADMAP-Punkt GESCHLOSSEN (Bernds Entscheidung 3 nachgezogen)
|
||||||
|
|
||||||
|
**Erledigt:** Folge aus Review-Befund (v): Bernds Entscheidungen 3 und 4 aus
|
||||||
|
`ANTWORTEN_BERND.md` sind jetzt überall nachgezogen, damit keine künftige
|
||||||
|
Session die geschlossenen Fragen wieder aufmacht:
|
||||||
|
|
||||||
|
- **ROADMAP M0 „Dashboard-Admin-Endpoints": abgehakt.** Das Heimnetz-Gate ist
|
||||||
|
laut Bernd der Endzustand (kein Fernzugriff, unterwegs WireGuard, A/B-Frage
|
||||||
|
geschlossen). Restrisiken (öffentliche Nutzerlisten-Ansicht, theoretisches
|
||||||
|
LAN-CSRF) bewusst akzeptiert – im ROADMAP-Punkt und in
|
||||||
|
`docs/DASHBOARD_AUTH_HARDENING.md` vermerkt.
|
||||||
|
- **`docs/DASHBOARD_AUTH_HARDENING.md`:** Status-Kopf auf „ABGESCHLOSSEN"
|
||||||
|
umgestellt, „Interims-Gate" heißt jetzt Endzustand, Weg A/B als Archiv
|
||||||
|
markiert, „bis zur A/B-Entscheidung"-Formulierungen bereinigt.
|
||||||
|
- **ROADMAP M0 Secrets-Punkt:** Vermerk zu Entscheidung 4 ergänzt – Rotation
|
||||||
|
und History-Rewrite ruhen bewusst (Bernds manuelle Sache, nicht vom
|
||||||
|
Autopilot anstoßen oder nachfragen); der Punkt bleibt nur als Merkposten
|
||||||
|
offen. KEINE Code-/Serveränderung in diesem Schritt, nur Doku –
|
||||||
|
`server.py` und das Gate sind unangetastet.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** In M0 sind damit nur noch zwei Punkte offen, beide
|
||||||
|
nicht Pi-bearbeitbar: SQLCipher (PC-/Gerätetest, laut Entscheidung 1 als
|
||||||
|
Erstes am PC) und der Härtetest (`docs/PC_TESTPLAN.md`). Danach M2 mit
|
||||||
|
Call-Pilot (Entscheidung 2, Gerätetest nötig). Auf dem Pi bleibt ohne neuen
|
||||||
|
Anlass nichts sicher Bearbeitbares – Session darf sauber enden.
|
||||||
|
|
||||||
|
**Stolperfallen:** Keine – reine Doku-Angleichung. Lehre aus dem
|
||||||
|
(v)-Befund: Wenn Entscheidungen in einer eigenen Datei landen
|
||||||
|
(`ANTWORTEN_BERND.md`), müssen die referenzierten offenen Punkte in
|
||||||
|
ROADMAP/Docs im SELBEN Zug geschlossen/annotiert werden, sonst stellt die
|
||||||
|
nächste Session die beantwortete Frage erneut.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-05 – Fable-5-Review (v)+(w): die zwei PC-Commits geprüft – Fix korrekt, 1 Doku-Lücke gefunden
|
||||||
|
|
||||||
|
**Erledigt:** Review-Pass über die seit (u) dazugekommene Arbeit – die zwei am
|
||||||
|
PC entstandenen Commits als Punkte (v) und (w) im Fable-5-Review-Abschnitt
|
||||||
|
abgehakt:
|
||||||
|
|
||||||
|
- **(v) 825481e (`ANTWORTEN_BERND.md` + CLAUDE.md-Verweis):** inhaltlich
|
||||||
|
konsistent – alle referenzierten Pläne/Dateien existieren
|
||||||
|
(`docs/SQLCIPHER_MIGRATION.md`, `docs/M2_MODULE_SCHNITT.md`,
|
||||||
|
`docs/DASHBOARD_AUTH_HARDENING.md`, `docs/PC_TESTPLAN.md`;
|
||||||
|
`voip_manager.dart`/`livekit_call_manager.dart` liegen in `lib/core/`), die
|
||||||
|
vier Entscheidungen decken exakt die in PROGRESS.md gesammelten Fragen ab.
|
||||||
|
**Befund (Doku-Lücke):** Entscheidung 3 (Dashboard bleibt Heimnetz-only,
|
||||||
|
A/B-Frage GESCHLOSSEN) war noch nicht in ROADMAP.md und
|
||||||
|
`docs/DASHBOARD_AUTH_HARDENING.md` nachgezogen – beide präsentierten die
|
||||||
|
Frage weiterhin als offen, künftige Sessions hätten Bernd erneut gefragt.
|
||||||
|
Direkt im nächsten Schritt dieser Session behoben (eigener Commit,
|
||||||
|
Eintrag oben).
|
||||||
|
- **(w) 4477ed9 (`autopilot.sh`, CLAUDE_CONFIG_DIR-Falle):** Fix ist korrekt
|
||||||
|
und am lebenden System belegt: Mit gesetztem `CLAUDE_CONFIG_DIR` sucht die
|
||||||
|
Claude-CLI ihre Config unter `$CLAUDE_CONFIG_DIR/.claude.json` – das
|
||||||
|
Hauptkonto-Profil liegt aber unter `~/.claude.json`, das alte
|
||||||
|
`export CLAUDE_CONFIG_DIR="$HOME/.claude"` zeigte also auf ein leeres
|
||||||
|
Zweitprofil (`~/.claude/.claude.json`). `unset` ist richtig und wirkt auch
|
||||||
|
nach einer vorherigen pyramid-Iteration derselben Shell (apply_account
|
||||||
|
läuft vor jeder Session). Lebender Beweis: DIESE Session lief als Konto
|
||||||
|
„haupt" (`~/.claude-accounts/active` = haupt, `CLAUDE_CONFIG_DIR` leer,
|
||||||
|
`~/.claude.json` vorhanden) und funktionierte. Kein weiterer
|
||||||
|
`CLAUDE_CONFIG_DIR`-Verweis im Repo. Kein Befund.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Befund aus (v) nachziehen (ROADMAP-Dashboard-Punkt
|
||||||
|
schließen, Doku-Status aktualisieren, Vermerk zu Entscheidung 4 am
|
||||||
|
Secrets-Punkt) – passiert direkt als nächster Schritt dieser Session.
|
||||||
|
|
||||||
|
**Stolperfallen:** Der CHANGES.md-Hook (in `.claude/settings.json`, schreibt
|
||||||
|
relativ nach `CHANGES.md`) protokolliert nur Arbeit auf der jeweiligen
|
||||||
|
Maschine – die PC-Commits vom 2026-07-05 haben hier auf dem Pi KEINE
|
||||||
|
Hook-Einträge hinterlassen. Der „Hook-Log als Detektor für unprotokollierte
|
||||||
|
Arbeit"-Trick aus Review (p) funktioniert also nur pro Maschine; für
|
||||||
|
PC-Arbeit bleibt `git log` die einzige Quelle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-05 – Bernds Richtungsfragen beantwortet (am PC)
|
||||||
|
|
||||||
|
**Erledigt:** Die über mehrere Sessions gesammelten „Fragen an Bernd" sind beantwortet
|
||||||
|
und in `ANTWORTEN_BERND.md` festgehalten (maßgeblich, in CLAUDE.md verlinkt):
|
||||||
|
1. SQLCipher → **vor M2** priorisieren (M1-Punkt); Migration braucht PC-/Gerätetest.
|
||||||
|
2. M2 erstes Modul → **Call-Pilot zuerst** (nicht `settings_modal.dart`); braucht Gerätetest.
|
||||||
|
3. Dashboard-Fernzugriff → **Heimnetz-only lassen**; A/B-Frage geschlossen.
|
||||||
|
4. Git-History / Token-Rotation → **vorerst nichts tun** (Bernds manuelle Sache).
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Die entschiedenen Punkte bleiben PC-/gerätegebunden
|
||||||
|
(SQLCipher-Migration, Call-Pilot-Umbau). Auf dem Pi ohne GUI nur sicher Vorbereitbares
|
||||||
|
angehen und klar als UNGETESTET markieren; Verhaltensrelevantes erst nach
|
||||||
|
`docs/PC_TESTPLAN.md` als erledigt abhaken. Reihenfolge: erst SQLCipher, dann Call-Pilot.
|
||||||
|
|
||||||
|
**Stolperfallen:** Beide priorisierten Punkte sind trotz Entscheidung NICHT auf dem Pi
|
||||||
|
abschließbar (kein GUI-/Gerätetest) – nicht als „jetzt endlich machbar" missverstehen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-07-04 – Fable-5-Review (u): (t)-Fix geprüft – korrekt, KEIN Befund, server.py-Review-Kette abgeschlossen
|
||||||
|
|
||||||
|
**Erledigt:** Review-Pass über die letzte noch ungeprüfte Arbeit – den
|
||||||
|
(t)-Restbefund-Fix (Commit d3e75c3) gegengelesen und als Punkt (u) im
|
||||||
|
Fable-5-Review-Abschnitt abgehakt. Wie im Arbeitsstand-Memory vorgesehen war
|
||||||
|
das ein kurzer Diff-Gegenlese-Pass, KEINE weitere Volltiefen-Runde:
|
||||||
|
|
||||||
|
- Diff gegen `server.py.bak-20260704-review-t` ist exakt der protokollierte
|
||||||
|
Mini-Fix (1 Fundstelle: `registration-status` antwortet generisch, Detail
|
||||||
|
per stderr ins Journal); `sys` ist importiert, `py_compile` sauber.
|
||||||
|
- Dienst läuft mit dem aktuellen Code (Start 19:19:40 NACH Datei-mtime
|
||||||
|
19:19:22), kein unprotokollierter Rest im CHANGES-Hook-Log (letzte Einträge
|
||||||
|
19:19–19:22 gehören zur (t)-Session, danach Commit 4c6efa2).
|
||||||
|
- Die 4 verbliebenen `str(e)`-Antworten liegen alle im `with ADMIN_LOCK:`-Block
|
||||||
|
HINTER dem Heimnetz-Gate (run-stats, create-invite, toggle-registration,
|
||||||
|
ban/unban) – absichtlich, LAN-only, für Bernd nützlich. Kein Handlungsbedarf.
|
||||||
|
- Kurz-Check des laufenden Dienstes: stats.html 200, registration-status 200
|
||||||
|
(`{"enabled": false}`), unbekannte Route 404.
|
||||||
|
|
||||||
|
**Damit ist die server.py-Härtungskette (o)–(u) KONVERGIERT** – ohne neuen
|
||||||
|
Anlass (neuer Code, neue Route) gibt es dort nichts mehr zu prüfen. ALLE als
|
||||||
|
erledigt markierten ROADMAP-/PROGRESS-Punkte sind reviewt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
|
||||||
|
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest, M2-Call/State) oder
|
||||||
|
Bernd (Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go). Auf dem Pi
|
||||||
|
ist nichts sicher Bearbeitbares mehr offen (Memory: nicht erneut suchen,
|
||||||
|
UNGETESTET-Stapel bewusst gedeckelt).
|
||||||
|
|
||||||
|
**Stolperfallen:** Keine – der Befund aus (t) war korrekt gefixt. Eine
|
||||||
|
Review-Kette braucht ein dokumentiertes Ende-Kriterium („konvergiert wenn der
|
||||||
|
letzte Diff nur noch den Vorbefund fixt und das Muster-grep leer ist"), sonst
|
||||||
|
reviewt jede Session den Review der Vorsession bis in alle Ewigkeit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Fable-5-Review (t): (s)-Fixes geprüft – korrekt, 1 kleiner Restbefund gefunden und gefixt
|
||||||
|
|
||||||
|
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – die zwei
|
||||||
|
(s)-Restbefund-Fixes (Commit 13b333c) kritisch gegengelesen und als Punkt (t)
|
||||||
|
im Fable-5-Review-Abschnitt abgehakt. Der (s)-Diff selbst ist korrekt (gegen
|
||||||
|
Backup `server.py.bak-20260704-review-s` verifiziert: ban/unban-Body-Lesen
|
||||||
|
sitzt vollständig VOR dem Lock inkl. Validierung, die generischen 500er auf
|
||||||
|
beiden öffentlichen POST-Routen stimmen; Dienst lief mit dem aktuellen Code,
|
||||||
|
kein unprotokollierter Rest im CHANGES-Hook-Log). Beim Ganzdatei-Gegenlesen
|
||||||
|
EIN Restbefund derselben Familie wie (s)-Befund 2:
|
||||||
|
|
||||||
|
1. **`GET /api/registration-status` leakte Exception-Texte:** Die Route ist
|
||||||
|
öffentlich erreichbar (in `do_GET` gibt es kein Heimnetz-Gate) und
|
||||||
|
antwortete bei Fehlern mit `str(e)` – ein Lesefehler an `conduit.toml`
|
||||||
|
hätte den internen Dateipfad an Unauthentifizierte verraten. (s) hatte nur
|
||||||
|
die zwei öffentlichen POST-Routen gefixt und die öffentliche GET-Route
|
||||||
|
übersehen. Fix: gleiches Muster – generische 500-Antwort („Interner
|
||||||
|
Fehler"), Detail per stderr ins Journal.
|
||||||
|
|
||||||
|
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
|
||||||
|
`py_compile` sauber, Diff gegen Backup minimal wie geplant
|
||||||
|
(`server.py.bak-20260704-review-t`). 8 Routen-Checks wie in (s) alle
|
||||||
|
unverändert (stats 200, registration-status 200, ban ohne Nutzer 400,
|
||||||
|
8-KB-Body 413, livekit-token ungültig 401, Diagnose falscher Token 403,
|
||||||
|
gespoofter Cf-Header 403, unbekannte Route 404). **500-Beweis (neu):**
|
||||||
|
`conduit.toml` für ~1 s per `chmod 000` unlesbar gemacht →
|
||||||
|
`{"error": "Interner Fehler"}` nach außen, `PermissionError` im Journal
|
||||||
|
(vorher wäre `[Errno 13] … '/home/steggi/matrix/conduit.toml'` – also der
|
||||||
|
Pfad – nach draußen gegangen); Rechte sofort wiederhergestellt (664), Route
|
||||||
|
danach wieder 200. Test-Deny-Zeile aus `security_alerts.log` wieder entfernt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
|
||||||
|
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
|
||||||
|
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
|
||||||
|
|
||||||
|
**Stolperfallen:** Bei einem Info-Leak-Fix nicht nur die Methode fixen, in
|
||||||
|
der der Befund auftauchte: (s) hatte `do_POST` gefixt, aber `do_GET` derselben
|
||||||
|
Datei hat auch öffentliche Routen mit demselben `str(e)`-Muster. Lehre: Nach
|
||||||
|
einem Musterfund (`str(e)` an Unauthentifizierte) einmal `grep str(e)` über
|
||||||
|
die GANZE Datei und jede Fundstelle als „öffentlich oder LAN-only?"
|
||||||
|
einordnen, statt nur die Nachbarroute zu fixen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Fable-5-Review (s): (r)-Fixes geprüft – korrekt, 2 kleinere Restbefunde gefunden und gefixt
|
||||||
|
|
||||||
|
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – die drei
|
||||||
|
(r)-Restbefund-Fixes (Commit 5ada0e5) kritisch gegengelesen und als Punkt (s)
|
||||||
|
im Fable-5-Review-Abschnitt abgehakt. Der (r)-Diff selbst ist korrekt (gegen
|
||||||
|
Backup `server.py.bak-20260704-review-r` verifiziert, Dienst lief mit dem
|
||||||
|
aktuellen Code). Beim Gegenlesen der GANZEN Datei (Lehre aus (o)) zwei
|
||||||
|
kleinere Restbefunde in `server.py` gefunden und gefixt:
|
||||||
|
|
||||||
|
1. **ban/unban las den Body INNERHALB des `ADMIN_LOCK`:** Das 4-KB-Limit aus
|
||||||
|
(r) deckelt nur die Größe – ein LAN-Client, der `Content-Length` ankündigt
|
||||||
|
und nichts schickt, hielt den Lock trotzdem bis zum 30-s-Socket-Timeout
|
||||||
|
fest (der (r)-Kommentar behauptete fälschlich, der Deckel verhindere das).
|
||||||
|
Seit (r) wartet auch der öffentliche GET-Leser von `registration-status`
|
||||||
|
auf diesen Lock → der hing mit. Fix: Body-Lesen/Validieren VOR den Lock
|
||||||
|
gezogen (Handler-Logik identisch, nur verschoben).
|
||||||
|
2. **Öffentliche Routen leakten Exception-Texte:** `livekit-token` und
|
||||||
|
`e2ee-diagnostics` antworteten Unauthentifizierten bei Fehlern mit
|
||||||
|
`str(e)` – das kann interne Pfade (`livekit.yaml`-Pfad bei
|
||||||
|
FileNotFoundError) oder interne Adressen preisgeben. Fix: generische
|
||||||
|
500-Antwort („Interner Fehler"), Details per stderr ins Journal. Die
|
||||||
|
Admin-Routen behalten `str(e)` (LAN-only, für Bernd nützlich).
|
||||||
|
|
||||||
|
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
|
||||||
|
`py_compile` sauber, Diff gegen Backup minimal wie geplant
|
||||||
|
(`server.py.bak-20260704-review-s`). 8 Routen-Checks wie in (r) alle
|
||||||
|
unverändert (stats 200, registration-status 200, ban ohne Nutzer 400,
|
||||||
|
8-KB-Body 413, `Content-Length: -5` 413, livekit-token ungültig 401,
|
||||||
|
Diagnose falscher Token 403, gespoofter Cf-Header 403). **Lock-Beweis (neu):
|
||||||
|
ban-Request mit angekündigtem, nie gesendetem Body + paralleler
|
||||||
|
`registration-status`-GET → Antwort in 15 ms statt Lock-Blockade** (vorher
|
||||||
|
hätte der GET bis zu 30 s gewartet). **500-Beweis (neu):** ungültiges JSON an
|
||||||
|
livekit-token → `{"error": "Interner Fehler"}`, Detail
|
||||||
|
(`JSONDecodeError(...)`) steht im Journal. Test-Deny-Zeile aus
|
||||||
|
`security_alerts.log` wieder entfernt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
|
||||||
|
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
|
||||||
|
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
|
||||||
|
|
||||||
|
**Stolperfallen:** (1) Ein Größen-Limit ist KEIN Zeit-Limit: `read(n)` mit
|
||||||
|
kleinem n blockiert trotzdem bis zum Socket-Timeout, wenn der Client nichts
|
||||||
|
schickt – blockierende I/O gehört grundsätzlich VOR einen Lock, nicht
|
||||||
|
hinein. (2) `str(e)` in Fehlerantworten öffentlicher Endpoints ist ein
|
||||||
|
Info-Leak (Pfade, interne Hosts) – Details gehören ins Journal, nach draußen
|
||||||
|
nur Generisches.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Fable-5-Review (r): ADMIN_LOCK-Umbau (q) geprüft – korrekt, aber 3 kleinere Restbefunde gefunden und gefixt
|
||||||
|
|
||||||
|
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – den ADMIN_LOCK-Umbau
|
||||||
|
aus (q) (Commit c8ff850) kritisch gegengelesen und als Punkt (r) im
|
||||||
|
Fable-5-Review-Abschnitt abgehakt. Der Umbau selbst ist korrekt (Locking-Struktur,
|
||||||
|
Routen-Reihenfolge, Handler-Logik unverändert). Drei kleinere Restbefunde in
|
||||||
|
`server.py` gefunden und gefixt:
|
||||||
|
|
||||||
|
1. **GET-Leser-Race (gleiche Familie wie (q), dort übersehen):** (q) hatte nur
|
||||||
|
POST-gegen-POST betrachtet – `GET /api/registration-status` liest
|
||||||
|
`conduit.toml` aber OHNE Lock, während `toggle-registration`/`create-invite`
|
||||||
|
die Datei per Truncate+Write in-place neu schreiben → seit multithreaded (p)
|
||||||
|
kann der Leser eine halb geschriebene/leere Datei sehen (Dashboard zeigt
|
||||||
|
transient falschen Registrierungs-Status). Fix: Lesen unter `ADMIN_LOCK`.
|
||||||
|
**Wichtige Erkenntnis dabei:** Der naheliegendere Fix (atomares `os.replace`
|
||||||
|
statt In-Place-Write) wäre FALSCH – `conduit.toml` ist als **einzelne Datei**
|
||||||
|
in den Container gemountet (`./conduit.toml:/etc/conduit.toml:ro`), ein
|
||||||
|
Replace tauscht den Inode und der Container sähe für immer die alte Datei.
|
||||||
|
Im Code als Kommentar festgehalten, damit das nie jemand „verbessert".
|
||||||
|
2. **Negative `Content-Length`:** `livekit-token`, `e2ee-diagnostics` und
|
||||||
|
`ban`/`unban` prüften nur `length > MAX` – ein negativer Wert wäre
|
||||||
|
durchgerutscht und `rfile.read(-n)` liest bis EOF (Thread hängt bis zum
|
||||||
|
30-s-Timeout). Fix: `length < 0` wird wie „zu groß" abgelehnt (413).
|
||||||
|
3. **`ban`/`unban` ohne Body-Limit + `random` für Einladungs-Token:**
|
||||||
|
ban/unban lasen beliebig große Bodies (und halten dabei seit (q) den
|
||||||
|
ADMIN_LOCK) → 4-KB-Deckel wie bei livekit-token. Einladungs-Token
|
||||||
|
(`create-invite`) kamen aus dem vorhersagbaren `random`-PRNG, obwohl sie
|
||||||
|
die Registrierung am Homeserver freischalten → `secrets.choice`.
|
||||||
|
|
||||||
|
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
|
||||||
|
`py_compile` sauber, Diff gegen Backup minimal wie geplant
|
||||||
|
(`server.py.bak-20260704-review-r`). 8 Routen-Checks: stats.html 200,
|
||||||
|
registration-status 200 (liest jetzt unter Lock), ban ohne Nutzer localhost →
|
||||||
|
400 „Kein Nutzer angegeben", **ban mit 8-KB-Body → 413 (neu)**, **livekit-token
|
||||||
|
mit `Content-Length: -5` → sofort 413 statt Hänger (neu)**, livekit-token
|
||||||
|
ungültiger Matrix-Token → 401, e2ee-diagnostics falscher Token → 403,
|
||||||
|
Admin-Route öffentlich über die Dashboard-Domain → 403. Test-Deny-Zeile aus
|
||||||
|
`security_alerts.log` wieder entfernt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
|
||||||
|
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
|
||||||
|
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
|
||||||
|
|
||||||
|
**Stolperfallen:** (1) Bei Nebenläufigkeits-Reviews nicht nur Schreiber gegen
|
||||||
|
Schreiber prüfen, sondern auch die LESER derselben Ressource – `do_GET` war in
|
||||||
|
(q) komplett außen vor. (2) „Atomares Schreiben per `os.replace`" ist bei
|
||||||
|
Docker-**Datei**-Bind-Mounts eine Falle: der Mount hängt am Inode, nach einem
|
||||||
|
Replace sieht der Container die alte Datei weiter. In-Place-Write ist dort
|
||||||
|
absichtlich richtig. (3) `Content-Length` kann negativ sein – wer nur nach oben
|
||||||
|
deckelt, lässt `read()` bis EOF blockieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Fable-5-Review (q): Threading-Härtung (p) tiefer geprüft – echte Nebenläufigkeits-Race in den Admin-Routen gefunden und mit ADMIN_LOCK behoben
|
||||||
|
|
||||||
|
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – die Threading-Umstellung
|
||||||
|
aus (p) (Commit 343545b, `ThreadingHTTPServer`) noch einmal kritisch gegengelesen und
|
||||||
|
als Punkt (q) im Fable-5-Review-Abschnitt abgehakt. Befund: Die Umstellung auf
|
||||||
|
multithreaded hat einen **echten Race erzeugt, den es vorher (single-threaded, alles
|
||||||
|
serialisiert) nicht geben konnte** – (p) hat nur den öffentlich beschreibbaren
|
||||||
|
Diagnose-Pfad per `DIAG_LOCK` abgesichert, die **Admin-Routen aber übersehen**:
|
||||||
|
- `toggle-registration` und `create-invite` machen ein Read-Modify-Write auf
|
||||||
|
`conduit.toml` **plus** `docker compose restart conduit` – zwei gleichzeitige Klicks
|
||||||
|
= verlorenes Update (lost update) + doppelter Container-Neustart.
|
||||||
|
- `send_admin()` schläft 4 s und liest danach die **letzten** Nachrichten aus dem
|
||||||
|
Admin-Raum zurück, um „die" Antwort zuzuordnen – zwei gleichzeitige Admin-Befehle
|
||||||
|
können sich vertauschte Antworten einfangen.
|
||||||
|
|
||||||
|
**Fix (`server.py`):** Neues `ADMIN_LOCK` (threading.Lock), das **nur** die
|
||||||
|
Admin-Mutationen serialisiert. Dafür `do_POST` minimal umgebaut: die beiden
|
||||||
|
öffentlichen Routen (`/api/livekit-token`, `/api/e2ee-diagnostics`) werden jetzt
|
||||||
|
**zuerst** behandelt und returnen, danach folgt das Heimnetz-Gate und der komplette
|
||||||
|
Admin-Dispatch in einem `with ADMIN_LOCK:`. Damit bleiben die öffentlichen Pfade
|
||||||
|
**nebenläufig** – der Slow-Loris-Schutz aus (p) ist unangetastet. Handler-Logik
|
||||||
|
wortwörtlich unverändert (nur verschoben/eingerückt, per `diff` gegen Backup
|
||||||
|
verifiziert). `python3 -m py_compile` sauber; Backup `server.py.bak-20260704-adminlock`.
|
||||||
|
|
||||||
|
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
|
||||||
|
8 Routen-Checks identisch wie vorher (stats 200; ban localhost → 400 „Kein Nutzer",
|
||||||
|
ban „aus dem Internet"/gespoofter Cf-Header → 403; livekit-token 401/400/413;
|
||||||
|
e2ee-diagnostics falscher Token 403; unbekannte Route 404). **Race-Beweis:** ein
|
||||||
|
`run-stats` allein 1388 ms, **zwei parallel 2633 ms** (≈ 2× → serialisiert; ohne
|
||||||
|
Lock wären es ~1400 ms). **Non-Blocking-Beweis:** ein öffentliches `livekit-token`
|
||||||
|
**während** ein Admin-`run-stats` den Lock hält → in **11 ms** beantwortet (nicht
|
||||||
|
blockiert). Test-Deny-Zeile aus `security_alerts.log` wieder entfernt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
|
||||||
|
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
|
||||||
|
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
|
||||||
|
|
||||||
|
**Stolperfallen:** Eine Threading-Umstellung ist nie „nur" ein Server-Konstruktor-
|
||||||
|
Tausch – sie macht **jeden** geteilten Zustand (Dateien, Read-Modify-Write,
|
||||||
|
Readback-nach-sleep) plötzlich race-fähig. (p) hatte richtig den einen öffentlich
|
||||||
|
beschreibbaren Pfad (`DIAG_LOCK`) gesehen, aber die Admin-Routen mit `conduit.toml`
|
||||||
|
und dem Admin-Raum-Readback sind derselbe Fall. Lehre: Nach „Server ist jetzt
|
||||||
|
multithreaded" ALLE schreibenden/zustandsändernden Handler durchgehen, nicht nur
|
||||||
|
den einen, der den Anlass gab.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Fable-5-Review (p): Interims-Gate geprüft (korrekt) + halbfertige Threading-Härtung einer abgebrochenen Session vollendet
|
||||||
|
|
||||||
|
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit (Interims-Gate,
|
||||||
|
Commits e97a748/455742d) als Punkt (p) im Fable-5-Review-Abschnitt abgehakt.
|
||||||
|
Das Gate selbst ist korrekt (Socket-IP als belastbare Prüfung, Cf-Header nur
|
||||||
|
Zweitschranke, Deny-Log gedeckelt, kein Angreifer-Text im Log). Dabei über das
|
||||||
|
CHANGES.md-Hook-Log entdeckt: Eine Session hat um 09:43 (NACH dem letzten
|
||||||
|
Commit) **drei unprotokollierte server.py-Edits** gemacht und wurde mitten in
|
||||||
|
einer Threading-Härtung abgebrochen – `threading`-Import + `DIAG_LOCK` waren da,
|
||||||
|
aber der Lock wurde **nirgends benutzt**, und die eigentliche Umstellung auf
|
||||||
|
`ThreadingHTTPServer` fehlte (der Kommentar behauptete sie schon). Obendrein
|
||||||
|
lief der Dienst noch mit dem Code von 09:14 – nichts davon war je geladen.
|
||||||
|
Die angefangene Härtung ist fachlich richtig (der single-threaded `HTTPServer`
|
||||||
|
ließ EINEN langsamen Client den ganzen Server blockieren – inkl. des öffentlich
|
||||||
|
erreichbaren `/api/livekit-token`), darum vollendet statt zurückgebaut:
|
||||||
|
`ThreadingHTTPServer` in `__main__`, Diagnose-Log-Kappen+Anhängen in den
|
||||||
|
`DIAG_LOCK` gewickelt (einziger öffentlich beschreibbarer Pfad), `timeout = 30`
|
||||||
|
der Vorsession belassen. Dienst-Neustart (kill/`Restart=always`), headless
|
||||||
|
verifiziert (11 Prüfungen): stats 200; Admin-Route localhost UND 192.168.178.71
|
||||||
|
→ 400 „Kein Nutzer angegeben"; öffentlich UND gespoofter Cf-Header → 403;
|
||||||
|
livekit-token ungültig → 401; Diagnose falscher Token 403, gültig 200 + im Log,
|
||||||
|
300 KB → 413; **Slow-Loris-Test: parallele Anfrage in 10 ms beantwortet,
|
||||||
|
während ein Client hing** (vorher unmöglich), hängender Body-Read nach exakt
|
||||||
|
~30 s serverseitig beendet, Dienst danach gesund. Test-Einträge aus beiden
|
||||||
|
Logs wieder entfernt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
|
||||||
|
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
|
||||||
|
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
|
||||||
|
|
||||||
|
**Stolperfallen:** (1) Das CHANGES.md-Hook-Log ist der zuverlässigste Detektor
|
||||||
|
für unprotokollierte Arbeit abgebrochener Sessions – Zeitstempel der letzten
|
||||||
|
Einträge mit dem letzten Commit vergleichen. (2) „Datei geändert" heißt NICHT
|
||||||
|
„läuft": Dienststart-Zeit (`systemctl status`) gegen Datei-mtime prüfen – hier
|
||||||
|
lagen 30 Minuten Code unausgeführt auf der Platte. (3) Ein definierter, aber
|
||||||
|
nie benutzter Lock ist ein Halbfertig-Marker, kein Schutz.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Dashboard-Befund (o): akutes Loch mit aussperr-sicherem Interims-Gate geschlossen
|
||||||
|
|
||||||
|
**Erledigt:** Der KRITISCHE Befund (o) (Dashboard-Admin-Endpoints ohne Auth,
|
||||||
|
öffentlich erreichbar) ist entschärft, OHNE Bernds A/B-Entscheidung
|
||||||
|
vorwegzunehmen und OHNE ihn auszusperren. Erkenntnis, die das möglich machte:
|
||||||
|
Öffentlicher Verkehr erreicht `server.py` (Port 8080) ausschließlich über den
|
||||||
|
Cloudflare-Tunnel-Container (Absender-IP 172.x + `Cf-*`-Header) – Bernds
|
||||||
|
Heimnetz-Zugriffe kommen dagegen direkt mit `192.168.*`-Socket-IP an. Daher
|
||||||
|
„Weg C" als Interims-Gate in `server.py`: Die 5 Admin-Routen (`/api/ban`,
|
||||||
|
`/api/unban`, `/api/toggle-registration`, `/api/create-invite`, `/api/run-stats`)
|
||||||
|
akzeptieren nur noch Anfragen mit Heimnetz-/localhost-Socket-IP (nicht spoofbar)
|
||||||
|
und ohne Cloudflare-Header; sonst 403 mit deutscher Erklärung inkl.
|
||||||
|
Heimnetz-URL – das Dashboard zeigt `d.error` per `alert()`, Bernd sieht also
|
||||||
|
genau, was zu tun ist. Abgelehnte Versuche landen größenbegrenzt (5-MB-Deckel,
|
||||||
|
Lehre aus Befund (l)) in `security_alerts.log` (nur Fixpfad + Socket-IP, kein
|
||||||
|
attacker-kontrollierter Text → kein HTML-Injection-Risiko in der
|
||||||
|
Dashboard-Anzeige). Dienst-Neustart per kill/`Restart=always`, headless
|
||||||
|
verifiziert (9 Prüfungen): öffentlich ban/toggle → 403, localhost UND
|
||||||
|
192.168.178.71 → 400 „Kein Nutzer angegeben" (Gate passiert, App-Validierung
|
||||||
|
erreicht), gespoofter `Cf-Connecting-Ip` aus dem LAN → 403, `GET /` öffentlich
|
||||||
|
200, `/api/livekit-token` 401 und `/api/e2ee-diagnostics` 403 wie bisher
|
||||||
|
(bleiben bewusst öffentlich, die App braucht sie). Test-Einträge aus dem
|
||||||
|
Alert-Log wieder entfernt. Rückbau jederzeit: `server.py.bak-20260704-dashgate`.
|
||||||
|
Doku/ROADMAP nachgezogen (`docs/DASHBOARD_AUTH_HARDENING.md`: Status, Weg C,
|
||||||
|
Restrisiken).
|
||||||
|
|
||||||
|
**Zusätzlicher Befund (für Bernds Secret-Rotation wichtig):** Das Gitea-Repo
|
||||||
|
einfach privat zu schalten ist KEINE schnelle Lösung für den Secret-Leak (k):
|
||||||
|
Der In-App-Updater (`lib/core/update_checker.dart`) holt Releases **anonym**
|
||||||
|
über die öffentliche Gitea-API – Repo privat = Utas installierte App findet
|
||||||
|
keine Updates mehr, und ein Fix ließe sich nicht mehr per Update verteilen
|
||||||
|
(Henne-Ei). In ROADMAP M0 beim Rotations-Punkt als Warnung vermerkt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Bernds Entscheidung bleibt für den FERNZUGRIFF
|
||||||
|
relevant (Weg A empfohlen – schließt auch die weiterhin öffentliche
|
||||||
|
Nutzerlisten-/Präsenz-Ansicht; Restrisiken im Doc: LAN-CSRF, offene Ansicht).
|
||||||
|
Danach ggf. Gate ergänzen/ersetzen. Sonst unverändert: übrige M0-Punkte
|
||||||
|
brauchen PC/Gerät oder Bernd (SQLCipher, Rotation, LiveKit-Client-Umstellung,
|
||||||
|
Härtetest).
|
||||||
|
|
||||||
|
**Stolperfallen:** (1) `stats.html` wird alle 5 Minuten von `stats.sh` (Cron)
|
||||||
|
NEU GENERIERT – Änderungen an der HTML-Datei sind flüchtig, UI-Änderungen
|
||||||
|
müssten in `stats.sh` (deshalb bewusst NUR server-seitig gefixt). (2) Auf dem
|
||||||
|
Pi laufen ZWEI cloudflared-Instanzen: die lokal konfigurierte
|
||||||
|
(`cloudflared.yml`, nur Jellyfin) und eine remote-verwaltete
|
||||||
|
(`cloudflared-pingvin`, Ingress liegt im Cloudflare-Dashboard, lokal nicht
|
||||||
|
einsehbar) – Letztere trägt `dashboard.`/`git.steggi-matrix.work`. Wer nur die
|
||||||
|
lokale YAML liest, hält die Subdomains fälschlich für tot. (3) Der
|
||||||
|
Header-Check allein würde nicht reichen (Header sind von Direktverbindern
|
||||||
|
spoofbar/wegzulassen) – die Socket-IP ist die belastbare Wahrheit, der
|
||||||
|
Cf-Header-Check ist nur die zweite Schranke, falls der Tunnel je im
|
||||||
|
Host-Netz läuft.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Fable-5-Review (n)+(o): LiveKit-Route live+geprüft, KRITISCHER Auth-Befund am Dashboard
|
||||||
|
|
||||||
|
**Erledigt:** Review-Pass über das, was seit (l)/(m) am Server dazugekommen ist –
|
||||||
|
zwei neue Punkte (n)/(o) im Fable-5-Review-Abschnitt abgehakt (Details dort).
|
||||||
|
- **(n):** Beim Gegenlesen von `server.py` gefunden, dass die geplante
|
||||||
|
LiveKit-Token-Route `POST /api/livekit-token` von einer vorher abgebrochenen
|
||||||
|
(unprotokollierten) Session **bereits live gebaut** wurde. Kritisch geprüft und
|
||||||
|
headless verifiziert: Auth per whoami greift (ungültig→401), Body-Limit (413),
|
||||||
|
gültig→200; das JWT unabhängig dekodiert und die **Signatur gegen `livekit.yaml`
|
||||||
|
gegengerechnet – gültig**; Identität = geprüfte Matrix-user_id (kein Spoofing).
|
||||||
|
Route ist gefahrlos additiv. Server-Seite des M0-LiveKit-Punkts damit erledigt;
|
||||||
|
Client-Umstellung + Rotation bleiben PC/Gerät (heiliger Call-Pfad).
|
||||||
|
- **(o) KRITISCH:** Derselbe Dashboard-Server hat für seine **Admin-Endpoints
|
||||||
|
(`/api/ban`, `/api/unban`, `/api/toggle-registration`, `/api/create-invite`,
|
||||||
|
`/api/run-stats`) KEINE Authentifizierung** und ist über
|
||||||
|
`https://dashboard.steggi-matrix.work` **öffentlich erreichbar**. Belegt per
|
||||||
|
`curl` (App-eigene 400-Antwort über die öffentliche URL, kein Login davor).
|
||||||
|
Ein Fremder könnte damit Uta (jeden Nutzer) sperren oder die offene
|
||||||
|
Registrierung am Homeserver aktivieren. Der Hostname ist über CT-Logs auffindbar.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** **Bernd muss entscheiden, WIE das Dashboard künftig
|
||||||
|
authentifiziert** – Weg A (Cloudflare Access vor die Subdomain, kein Code, kein
|
||||||
|
Aussperr-Risiko, empfohlen) oder Weg B (eigener `DASH_TOKEN` + `stats.html`-Prompt).
|
||||||
|
Fertiger Plan mit beiden Wegen: `docs/DASHBOARD_AUTH_HARDENING.md`. Danach sofort
|
||||||
|
umsetzbar + headless verifizierbar. Nicht blind gefixt, weil jede Auth-Ergänzung
|
||||||
|
Bernds eigenes Admin-Panel bis zur Token-Eingabe lahmlegt („kein Aussperren") –
|
||||||
|
das mitten in einer autonomen Session zu deployen ist die falsche Reihenfolge.
|
||||||
|
|
||||||
|
**Stolperfallen:** Ein Review, der sich auf den *geänderten* Endpoint (Diagnose,
|
||||||
|
Disk-Fill) konzentriert, übersieht leicht die *unveränderten Nachbarn* in
|
||||||
|
derselben Datei. Lehre: bei einem Sicherheits-Review einer Datei nicht nur den
|
||||||
|
Diff prüfen, sondern die ganze Angriffsfläche der Datei (hier: alle Routen des
|
||||||
|
`do_POST`-Handlers auf Auth abklopfen, nicht nur die neu berührte).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Fable-5-Review (l)+(m): Diagnose-Endpoint hatte kein Größenlimit (gefixt)
|
||||||
|
|
||||||
|
**Erledigt:** Review-Pass über die zwei seit (k) dazugekommenen Arbeiten – als
|
||||||
|
Punkte (l) und (m) im Fable-5-Review-Abschnitt abgehakt (Details dort). Bei (l)
|
||||||
|
ein **echter Befund** am Server-Gegenpart des Admin-Token-Fixes: Der neue
|
||||||
|
`DIAG_TOKEN` steht bewusst im öffentlichen Repo (eng begrenzt, nur Log-Anhängen) –
|
||||||
|
aber `/api/e2ee-diagnostics` in `/home/steggi/matrix/server.py` las den
|
||||||
|
Request-Body **ohne Größenlimit** und hängte ihn unbegrenzt an die Log-Datei an.
|
||||||
|
Jeder Fremde hätte damit die Pi-Platte volllaufen lassen können (Disk-Fill-DoS
|
||||||
|
auf dem Host, der auch den Homeserver trägt). Gefixt in `server.py`:
|
||||||
|
Body-Limit 256 KB pro Upload (413 bei Überschreitung) + harte Obergrenze 20 MB
|
||||||
|
für die Log-Datei (ältere Hälfte fliegt raus). Headless verifiziert nach
|
||||||
|
Dienst-Neustart (kill, `Restart=always`): stats.html 200, gültiger Upload 200
|
||||||
|
und im Log gelandet, falscher Token 403, 300-KB-Body 413; Kappungslogik isoliert
|
||||||
|
getestet (100 Zeilen → 50, neueste überleben). Test-Einträge wieder entfernt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Weiter mit dem obersten offenen ROADMAP-Punkt
|
||||||
|
(M0 LiveKit-Token): Server-Route nach `docs/LIVEKIT_TOKEN_MIGRATION.md` bauen –
|
||||||
|
rein additiv, Client bleibt unangetastet (heiliger Call-Pfad wartet auf PC/Gerät).
|
||||||
|
|
||||||
|
**Stolperfallen:** Ein „eng begrenzter" Token im öffentlichen Repo ist nur dann
|
||||||
|
wirklich harmlos, wenn das Endpoint dahinter auch Missbrauch begrenzt
|
||||||
|
(Größe/Menge) – „keine Admin-Rechte" allein reicht nicht. Bei künftigen
|
||||||
|
Endpoints mit öffentlich bekanntem Token immer an Body-/Speicher-Limits denken.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – LiveKit-Token-Minting: Migrationsplan geschrieben (Prep, kein Code)
|
||||||
|
|
||||||
|
**Erledigt:** Für den offenen M0-Punkt „LiveKit-Token server-seitig minten"
|
||||||
|
(letzter Client-eingebetteter Secret nach der Diagnose-Entkopplung) den fertigen
|
||||||
|
Umsetzungsplan `docs/LIVEKIT_TOKEN_MIGRATION.md` geschrieben – gegen den echten
|
||||||
|
Client-Code (`livekit_token.dart`, `livekit_call_manager.dart:154`) und die echte
|
||||||
|
Pi-Konfig (`livekit.yaml`, `server.py`, Continuwuity `whoami`). Kern: Token-Route
|
||||||
|
`POST /api/livekit-token` am bestehenden Dashboard-Server, die den Matrix-Access-Token
|
||||||
|
per `whoami` prüft und daraus die LiveKit-Identität ableitet (kein Spoofing), JWT
|
||||||
|
per stdlib-HMAC mintet (kein neues Paket); Client holt nur noch das fertige JWT und
|
||||||
|
`apiSecret` verschwindet aus dem App-Code. Analog zum bewährten
|
||||||
|
`SQLCIPHER_MIGRATION.md`-Muster.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Umsetzung braucht echten Call-Test auf einem Gerät
|
||||||
|
(Calls „heilig", auf dem Pi kein GUI) – daher bewusst nur der Plan, kein Code.
|
||||||
|
Danach Secret-Rotation zusammen mit einem Release. Verbleibende M0-Secrets
|
||||||
|
(Admin-Token, Gitea-, Giphy-Token) brauchen Bernds Konten-Zugriff.
|
||||||
|
|
||||||
|
**Stolperfallen:** Keine – reine Doku.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Admin-Token aus dem Client entfernt (E2EE-Diagnose entkoppelt)
|
||||||
|
|
||||||
|
**Erledigt:** Den kritischsten Teil des Secret-Befunds (k) wirklich behoben, statt
|
||||||
|
ihn nur zu dokumentieren – und zwar sicher, weil diese Session direkt auf dem Pi
|
||||||
|
läuft, der den Dashboard-Server hostet (frühere Sessions kannten den Server-Code
|
||||||
|
nicht). Kernursache gefunden: `/home/steggi/matrix/server.py` benutzte für das
|
||||||
|
Diagnose-Endpoint `/api/e2ee-diagnostics` **denselben String** wie den
|
||||||
|
Matrix-Admin-Token (`DIAG_TOKEN = TOKEN`). Deshalb musste der Client diesen Token
|
||||||
|
mitliefern – d. h. jeder App-Nutzer (auch Uta) trug den vollen Server-Admin-Token
|
||||||
|
im Gerät.
|
||||||
|
|
||||||
|
- **Server (`server.py`):** `DIAG_TOKEN` ist jetzt ein eigener, zufälliger,
|
||||||
|
eng begrenzter Token (`pyr-diag-…`), unabhängig vom Admin-`TOKEN`. Das
|
||||||
|
Diagnose-Endpoint hängt nur an den Log an (keine Admin-Rechte) – der neue Token
|
||||||
|
ist also von geringer Sensibilität. Der Admin-`TOKEN` (für Ban/Invite/
|
||||||
|
Registrierung) bleibt **unverändert**, damit das Dashboard weiterläuft.
|
||||||
|
- **Client (`settings_encryption.dart`):** `_uploadDiagnostics` schickt jetzt den
|
||||||
|
neuen Diagnose-Token statt des Admin-Tokens. **`grep J5lax lib/` ist jetzt leer**
|
||||||
|
– der Admin-Token ist raus aus dem App-Code.
|
||||||
|
- **Verifiziert (headless, kein GUI nötig):** Dashboard-Server nach Neustart
|
||||||
|
(Prozess gekillt, systemd `Restart=always` lud den neuen Code): `stats.html` 200,
|
||||||
|
`registration-status` 200, `/api/e2ee-diagnostics` mit neuem Token **200**, mit
|
||||||
|
altem Admin-Token **403**. `flutter analyze` unverändert (1 bekannter Hinweis),
|
||||||
|
`flutter test` 24/24 grün. Meine Test-Einträge aus dem Diagnose-Log wieder
|
||||||
|
entfernt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Die **Rotation des Admin-Tokens `J5lax…` bleibt
|
||||||
|
Pflicht und Bernds Sache** – der Token ist weiterhin gültig und steht in der
|
||||||
|
Git-History sowie in Utas bereits installierter APK. Erst nach der Rotation ist
|
||||||
|
der Leak wirklich wertlos. Ebenso offen: LiveKit-`apiSecret` (server-seitiges
|
||||||
|
Minting), Gitea- und Giphy-Token (siehe ROADMAP M0). **Wichtig für den nächsten
|
||||||
|
PC-Lauf / das nächste Release:** Utas aktuell installierte App schickt beim
|
||||||
|
Diagnose-Upload noch den alten Token → dieser Button liefert bei ihr jetzt 403,
|
||||||
|
bis sie ein neues Release bekommt (nur die Diagnose-Funktion betroffen, nichts
|
||||||
|
Heiliges). Der Diagnose-Upload-Button gehört damit in den PC-Testplan (mit neuem
|
||||||
|
Build einmal drücken, Log auf dem Pi prüfen).
|
||||||
|
|
||||||
|
**Stolperfallen:** `systemctl restart matrix-stats.service` scheiterte an
|
||||||
|
fehlender polkit-Auth (kein Terminal für sudo). Lösung ohne root: den von systemd
|
||||||
|
verwalteten Prozess killen – `Restart=always`/`RestartSec=5` lädt server.py neu
|
||||||
|
(neue MainPID bestätigt). Nur anwendbar, weil der Dienst als User `steggi` läuft
|
||||||
|
und der eigene Prozess killbar ist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Fable-5-Review (k): Secret-Scan – ECHTER Sicherheitsbefund, teils gefixt
|
||||||
|
|
||||||
|
**Erledigt:** Beim Review-Pass zusätzlich einen systematischen Secret-Scan über
|
||||||
|
`lib/`, `scripts/`, `docs/`, `android/` gemacht (CLAUDE.md-Leitplanke „Keine
|
||||||
|
Secrets ins Repo"). **Ergebnis: mehrere echte, aktive Zugangsdaten liegen im
|
||||||
|
Repo – und das Gitea-Repo ist als `private:false` unter der Internet-Domain
|
||||||
|
`git.steggi-matrix.work` erreichbar** (per `curl` bestätigt: Repo-Metadaten und
|
||||||
|
Releases anonym abrufbar). Damit sind alle folgenden Secrets als **kompromittiert
|
||||||
|
zu behandeln** (seit Commit 25ed765 in der History):
|
||||||
|
|
||||||
|
- **Matrix-Server-ADMIN-Token** `J5lax…` – lag doppelt drin:
|
||||||
|
(1) als Bearer-Token in `docs/matrix-sdk/13-server-admin-continuwuity.md`
|
||||||
|
(19 Stellen) und (2) **im ausgelieferten App-Code**
|
||||||
|
`lib/widgets/settings/settings_encryption.dart:431` (E2EE-Diagnose-Upload).
|
||||||
|
Per `whoami` gegen den Homeserver geprüft: **Token ist noch gültig**
|
||||||
|
(`@todesneutron:steggi-matrix.work`, voller Server-Admin). Kritischster Fund –
|
||||||
|
jeder App-Nutzer (auch Uta) trägt den Server-Admin-Token im Client.
|
||||||
|
- **Gitea-Release-Token** `bb522f96…` (Repo-Schreibrechte) hartcodiert in
|
||||||
|
`scripts/release.ps1:14`.
|
||||||
|
- **LiveKit-`apiSecret`** `rYUT2PRa…` in `lib/core/livekit_token.dart:6` – die
|
||||||
|
App mintet LiveKit-JWTs **client-seitig**, das SFU-Secret muss also im Client
|
||||||
|
liegen (Architekturproblem, nicht nur ein Repo-Leak).
|
||||||
|
- **Giphy-API-Key** `QtEy…` in `lib/features/chat/gif_sticker_picker.dart:9`
|
||||||
|
(geringe Sensibilität, aber ebenfalls im Client).
|
||||||
|
|
||||||
|
**Sofort (gefahrlos, kein Laufzeit-Bezug) bereinigt:**
|
||||||
|
- `scripts/release.ps1`: Token raus, kommt jetzt aus Umgebungsvariable
|
||||||
|
`PYRAMID_GITEA_TOKEN`; fehlt sie, bricht das Skript mit klarer Anleitung ab.
|
||||||
|
- `docs/matrix-sdk/13-server-admin-continuwuity.md`: alle 19 Token-Stellen durch
|
||||||
|
`<ADMIN_TOKEN>`-Platzhalter ersetzt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt (BRAUCHT BERND / PC – nicht blind auf dem Pi fixen):**
|
||||||
|
1. **Alle vier Secrets rotieren** (sie sind bereits geleakt, egal was im Code
|
||||||
|
steht): neuen Matrix-Admin-Token erzeugen und alten widerrufen; neuen
|
||||||
|
Gitea-Token; LiveKit-`apiKey`/`apiSecret` neu setzen (Hetzner-LiveKit +
|
||||||
|
`ice.json`); Giphy-Key neu ziehen. **Das ist der wichtigste Schritt** – die
|
||||||
|
Repo-Bereinigung allein nützt nichts, solange die alten Werte gültig sind.
|
||||||
|
2. **Client-seitige Secrets entschärfen (Architektur):** LiveKit-Token gehört
|
||||||
|
server-seitig gemintet (kleiner Token-Endpoint auf dem Pi, App holt sich nur
|
||||||
|
das fertige JWT) – dann verschwindet `apiSecret` aus dem Client. Der
|
||||||
|
E2EE-Diagnose-Upload darf **niemals** den Server-Admin-Token benutzen: eigenen,
|
||||||
|
eng begrenzten Token (nur der Dashboard-Endpoint) oder besser einen
|
||||||
|
unauthentifizierten Upload mit Rate-Limit. Beides als ROADMAP-M0-Punkt neu
|
||||||
|
aufgenommen. Blind entfernen verbietet sich, weil Calls/Diagnose sonst brechen
|
||||||
|
(Calls sind „heilig") und auf dem Pi kein GUI-Test möglich ist.
|
||||||
|
3. **Git-History:** Die alten Werte bleiben in der Gitea-History sichtbar. Nach
|
||||||
|
der Rotation sind sie wertlos; ein History-Rewrite (force-push) ist optional
|
||||||
|
und Bernds Entscheidung (das Repo dient auch als Backup) – daher NICHT
|
||||||
|
eigenmächtig gemacht.
|
||||||
|
|
||||||
|
**Stolperfallen:** Der Diagnose-„Token" in `settings_encryption.dart` ist
|
||||||
|
NICHT ein harmloser Dashboard-Key, sondern derselbe String wie der
|
||||||
|
Matrix-Server-Admin-Token – beim Lesen leicht zu übersehen, weil er dort nur
|
||||||
|
`token:` heißt. Lehre: bei Secret-Funden immer prüfen, ob derselbe Wert an
|
||||||
|
mehreren Stellen (Doku UND Client) wiederkehrt und WAS er wirklich autorisiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-03 – Fable-5-Review (h)–(j): Test-/Doku-Commits geprüft, 1 echter Befund gefixt
|
||||||
|
|
||||||
|
**Erledigt:**
|
||||||
|
- **Review-Pass über die drei seit (g) dazugekommenen Commits** – als Punkte
|
||||||
|
(h)–(j) im Fable-5-Review-Abschnitt abgehakt (Details dort): AuthLog-Tests
|
||||||
|
e0ac5cb korrekt (h), PC-Testplan-Update 03b84e0 korrekt (j), und bei den
|
||||||
|
settings_prefs-Tests 7dfdbf1 (i) beim Gegenlesen des Produktivcodes ein
|
||||||
|
**echter Befund**: Lade-Race in `BoolPref`/`StringSetPref` –
|
||||||
|
`StringSetPref.add()` vor abgeschlossenem `_load` konnte **bereits
|
||||||
|
persistierte Einträge löschen** (Read-Modify-Write auf ungeladenem State;
|
||||||
|
per Regressionstest am alten Code bewiesen, dann gefixt: 7b90f20).
|
||||||
|
Vorgehen: Test zuerst geschrieben, Fehlschlag am alten Code gesehen, dann
|
||||||
|
Fix, dann alles grün – kein blinder Fix.
|
||||||
|
- Unabhängige Verifikation: `flutter test` jetzt **24/24 grün auf dem Pi**
|
||||||
|
(2 neue Lade-Race-Tests), `flutter analyze` unverändert 1 bekannter Hinweis
|
||||||
|
(`chat_provider.dart:42`). PC-Testplan Schritt 0 auf 24 Tests nachgezogen.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert blockiert – alle offenen ROADMAP-Punkte
|
||||||
|
(M0 SQLCipher, M1 Härtetest, M2 Call-Schicht/State-Management) brauchen PC/GUI
|
||||||
|
oder Bernds Antworten (Call-Pilot, SQLCipher-Priorität), siehe
|
||||||
|
`docs/PC_TESTPLAN.md` und „Fragen an Bernd" weiter unten. Der Lade-Race-Fix
|
||||||
|
ist headless voll getestet, gehört aber der Vollständigkeit halber mit in den
|
||||||
|
nächsten PC-Klickdurchlauf (Einstellungs-Toggles, Space-Beitritt).
|
||||||
|
|
||||||
|
**Stolperfallen:** Der BoolPref-Teil des Race ist im Test-Mock NICHT
|
||||||
|
reproduzierbar (Microtask-Reihenfolge dort zufällig gutartig: `set()`
|
||||||
|
persistiert vor dem `_load`-Read) – nur der StringSetPref-Teil schlug real
|
||||||
|
fehl. Lehre: Bei Race-Befunden nicht vom „Test wird schon rot"-Reflex leiten
|
||||||
|
lassen, sondern die konkrete Microtask-Reihenfolge durchdenken; ein Guard kann
|
||||||
|
richtig sein, auch wenn der Test ihn in dieser Umgebung nicht erzwingen kann
|
||||||
|
(dokumentiert im Review-Punkt (i)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-03 – Review-Nachzügler abgeschlossen + AuthLog-Regressionstests
|
||||||
|
|
||||||
|
**Erledigt:**
|
||||||
|
- **Fable-5-Review vervollständigt (eebf8ae):** Die drei seit Review-Abschluss
|
||||||
|
dazugekommenen erledigten Arbeiten kritisch geprüft und als Punkte (e)–(g) im
|
||||||
|
Review-Abschnitt abgehakt: Unit-Tests 99cde9a (korrekt, Fake-Clients schlagen
|
||||||
|
bei jedem Fremd-Aufruf laut fehl), Doku-Commits 16561bb/566938f (alle
|
||||||
|
Behauptungen gegen Code/pubspec/matrix-6.2.0 verifiziert – Factory-Stellen,
|
||||||
|
Tabellenname `box_inbound_group_session`), Autopilot-Konto-Wechsel bfe5876
|
||||||
|
(kein App-Code, Logik in Ordnung; harmloser False-Positive der
|
||||||
|
Limit-Erkennung dokumentiert). **Kein Befund, keine Code-Änderung nötig.**
|
||||||
|
Damit sind ALLE als erledigt markierten Punkte reviewt.
|
||||||
|
- **`test/auth_log_test.dart` (4 Tests):** `AuthLog` ist die einzige weitere
|
||||||
|
öffentliche Schnittstelle auf dem heiligen Pfad, die headless testbar ist
|
||||||
|
(der Soft-Logout-Guard ruft `AuthLog.write` im catch-Pfad auf – würde es
|
||||||
|
werfen, eskalierte das SDK zu `logout()`+`clear()`). Getestet: write/read
|
||||||
|
werfen NIE, auch wenn die Plattform fehlt (kaputter PathProvider);
|
||||||
|
Platzhalter statt Fehler bei leerem Log; ISO-Zeitstempel pro Zeile;
|
||||||
|
500-Zeilen-Kappung (älteste Einträge fliegen raus, neueste überleben).
|
||||||
|
Dafür `path_provider_platform_interface ^2.1.2` als dev_dependency ergänzt –
|
||||||
|
offizieller Mock-Weg, war schon transitiv im Lockfile, kein neues externes
|
||||||
|
Paket. `flutter test`: alle 14 Tests grün, `flutter analyze`
|
||||||
|
unverändert (1 bekannter Hinweis `chat_provider.dart:42`).
|
||||||
|
- **`test/settings_prefs_test.dart` (8 Tests):** `core/settings_prefs.dart`
|
||||||
|
ist die Storage-Fassade, die laut `docs/M2_MODULE_SCHNITT.md` zur
|
||||||
|
Pflicht-Schnittstelle ausgebaut werden soll – dieses Vertragsnetz muss beim
|
||||||
|
M2-Umbau stehen bleiben. Getestet mit dem offiziellen In-Memory-Mock
|
||||||
|
(`SharedPreferences.setMockInitialValues`, kein neues Paket): `BoolPref`
|
||||||
|
(persistierter Wert überstimmt Default, set/toggle persistieren),
|
||||||
|
`StringSetPref` (Laden/add/remove, No-Ops bei Duplikat/Fehlendem),
|
||||||
|
Theme-Prefs (Defaults, Roundtrip, Teil-Save lässt Rest unangetastet),
|
||||||
|
Server-Banner (Roundtrip, Entfernen per null/leer, **korruptes JSON liefert
|
||||||
|
leere Map statt Crash** – echter Fehlerpfad). `flutter test`: **alle
|
||||||
|
22 Tests grün auf dem Pi.**
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert blockiert: Alle offenen ROADMAP-Punkte
|
||||||
|
(M0 SQLCipher, M1 Härtetest, M2 Call-Schicht/State-Management) brauchen PC/GUI
|
||||||
|
oder Bernds Antworten (Call-Pilot, SQLCipher-Priorität) – siehe
|
||||||
|
`docs/PC_TESTPLAN.md` und die Fragen an Bernd weiter unten. Weitere sichere
|
||||||
|
Pi-Testkandidaten sind jetzt wirklich erschöpft (Rest ist bibliotheksprivat
|
||||||
|
oder braucht Widget-Tests mit gemocktem Matrix-Client – lohnt erst nach dem
|
||||||
|
M2-Modulschnitt mit echten öffentlichen Schnittstellen).
|
||||||
|
|
||||||
|
**Stolperfallen:** `AuthLog` cached sein File-Handle statisch – der Test für
|
||||||
|
„Plattform kaputt" muss deshalb VOR den Tests mit funktionierender Plattform
|
||||||
|
laufen (im Testfile kommentiert). Bei künftigen Tests an Singletons mit
|
||||||
|
statischem Zustand auf Testreihenfolge/Isolate-Grenzen achten (flutter test
|
||||||
|
isoliert pro Datei, nicht pro Test).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-03 – Erste Unit-Tests: Regressionsnetz für die zwei heiligsten Codepfade
|
||||||
|
|
||||||
|
**Erledigt:** Eine Vorsession hatte (unprotokolliert, Abbruch vor dem Testlauf –
|
||||||
|
sichtbar nur am untrackten `test/`-Ordner + CHANGES.md-Hook-Log 18:16/18:17)
|
||||||
|
zwei Testdateien angelegt. Diese Session hat sie gegen den echten Code
|
||||||
|
gegengelesen, ausgeführt und committet (99cde9a) – damit hat das Projekt erstmals
|
||||||
|
überhaupt eine Testsuite, und zwar genau dort, wo CLAUDE.md „heilig" sagt:
|
||||||
|
|
||||||
|
- `test/soft_logout_guard_test.dart` (4 Tests): `guardedSoftLogoutRefresh`
|
||||||
|
wirft NIE nach außen (weder Exception noch Error) – ein Throw würde im SDK
|
||||||
|
zu `logout()`+`clear()` eskalieren (der Uta-Bug). Geprüft: Erfolg beim
|
||||||
|
1. Versuch = genau 1 Aufruf, transienter Fehler wird per Retry geheilt,
|
||||||
|
dauerhafter Fehler = genau 3 Versuche + normale Rückkehr. Der Fake-Client
|
||||||
|
schlägt bei JEDEM anderen SDK-Aufruf laut fehl – riefe der Guard je
|
||||||
|
`logout()`/`clear()` auf, fiele der Test sofort um.
|
||||||
|
- `test/power_levels_safety_test.dart` (6 Tests): `updatePowerLevelsSafely`
|
||||||
|
(Selbst-Aussperr-Schutz im Space-Admin) – Server-Stand bleibt erhalten
|
||||||
|
(fremde users-Einträge überleben; genau der Bug, der schon mal Admins
|
||||||
|
ausgesperrt hat), Selbst-Degradierung bricht ab ohne zu schreiben,
|
||||||
|
users_default-Fallback, explizites `events[m.room.power_levels]`-
|
||||||
|
Erfordernis, M_NOT_FOUND-Start mit leerem Event, fremde Serverfehler
|
||||||
|
(M_FORBIDDEN) werden durchgereicht.
|
||||||
|
- `flutter test`: **alle 10 Tests grün auf dem Pi** (läuft headless, im
|
||||||
|
Gegensatz zu `flutter run` – d. h. dieser Punkt ist NICHT „UNGETESTET (Pi)").
|
||||||
|
`flutter analyze` unverändert 1 bekannter Hinweis (`chat_provider.dart:42`).
|
||||||
|
- Wert fürs Refactoring: Beide Funktionen sind Vertragsgrenzen, die bei
|
||||||
|
M2-Umbauten (Call-Fassade, Storage) nicht wackeln dürfen – künftige
|
||||||
|
Sessions sollten `flutter test` fest in Schritt 3 des Arbeitszyklus
|
||||||
|
aufnehmen (geht auch auf dem Pi).
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert: PC-Termin (docs/PC_TESTPLAN.md) oder
|
||||||
|
Bernds Antworten (Call-Pilot, SQLCipher-Priorität). Weitere Test-Kandidaten
|
||||||
|
scheitern derzeit an der Sichtbarkeit: `_detectType`, `_formatMessageTime`,
|
||||||
|
`_compareSpaceChildren`, die Megolm-Export-Kryptohelfer usw. sind alle
|
||||||
|
bibliotheksprivat – testbar erst, wenn beim M2-Modulschnitt echte öffentliche
|
||||||
|
Schnittstellen entstehen (dann Tests gleich mitschreiben).
|
||||||
|
|
||||||
|
**Stolperfallen:** Die Vorsession wurde mitten im Schritt abgebrochen, OHNE
|
||||||
|
den Zwischenstand zu protokollieren (Pflicht laut CLAUDE.md) – der untrackte
|
||||||
|
`test/`-Ordner war nur per `git status` auffindbar. Lehre: `git status` am
|
||||||
|
Sessionstart ernst nehmen; untrackte Dateien können unprotokollierte,
|
||||||
|
halbfertige Arbeit einer abgebrochenen Session sein.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-07-03 – M1-Vorbereitung: SQLCipher-Migrationsplan + konsolidierter PC-Testplan
|
## 2026-07-03 – M1-Vorbereitung: SQLCipher-Migrationsplan + konsolidierter PC-Testplan
|
||||||
|
|
||||||
**Erledigt:** Nach Abschluss des Fable-5-Reviews (Abschnitt darunter) die zwei
|
**Erledigt:** Nach Abschluss des Fable-5-Reviews (Abschnitt darunter) die zwei
|
||||||
@@ -130,6 +868,228 @@ schließt den Review ab.
|
|||||||
eingefrorener Alt-Stand (2026-06-04) mit Verwechslungsgefahr zum echten
|
eingefrorener Alt-Stand (2026-06-04) mit Verwechslungsgefahr zum echten
|
||||||
Protokoll im Root → Archiv-Banner eingefügt.
|
Protokoll im Root → Archiv-Banner eingefügt.
|
||||||
|
|
||||||
|
- [x] **(e) Unit-Tests (99cde9a) gegengelesen – korrekt, keine Änderung nötig.**
|
||||||
|
Beide Fake-Clients schlagen bei jedem nicht implementierten SDK-Aufruf laut
|
||||||
|
fehl (NoSuchMethodError) – ein versehentlicher `logout()`/`clear()`-Aufruf im
|
||||||
|
Guard würde sofort auffallen. `soft_logout_guard_test`: deckt Erfolg,
|
||||||
|
transienten Fehler (Retry), dauerhaften Fehler (genau 3 Versuche, kein Throw)
|
||||||
|
und `Error` (nicht nur `Exception`) ab – passt zum `catch (e)` im Guard, das
|
||||||
|
bewusst alles fängt. Dass die Tests grün sind, beweist nebenbei, dass
|
||||||
|
`AuthLog.write` ohne path_provider-Plattform (Testumgebung) sauber schluckt
|
||||||
|
statt zu werfen – genau die Eigenschaft, an der der Guard hängt.
|
||||||
|
`power_levels_safety_test`: alle 6 Fälle gegen den echten Code
|
||||||
|
(`space_admin_core.dart:10-56`) nachvollzogen, inkl. der wichtigen Kante
|
||||||
|
„explizites `events[m.room.power_levels]` schlägt `state_default`".
|
||||||
|
Einzige Anmerkung: die zwei Retry-Tests schlafen real 2s+4s (Suite ~14s) –
|
||||||
|
bewusst so gelassen, statt für Testbarkeit einen Delay-Parameter in den
|
||||||
|
heiligen Produktivpfad zu bohren.
|
||||||
|
- [x] **(f) Doku-Commits 16561bb/566938f (SQLCipher-Plan, PC-Testplan) gegen
|
||||||
|
Code verifiziert – Behauptungen stimmen:** Factory-Stellen exakt wie
|
||||||
|
dokumentiert (`matrix_client.dart:24` nativ Android/iOS, `:27` FFI Desktop;
|
||||||
|
`background_push.dart:188` nativ), `sqlcipher_flutter_libs ^0.6.8` +
|
||||||
|
`flutter_secure_storage ^10` in pubspec vorhanden/ungenutzt wie beschrieben,
|
||||||
|
Verifikations-Tabellenname `box_inbound_group_session` steht wirklich in
|
||||||
|
matrix-6.2.0 `matrix_sdk_database.dart:139`, beide Prozesse öffnen dieselbe
|
||||||
|
`pyramid.sqlite` mit WAL + busy_timeout 5000. PC-Testplan deckt alle in
|
||||||
|
PROGRESS verstreuten UNGETESTET-Punkte ab (stichprobenartig gegengeprüft).
|
||||||
|
- [x] **(g) Autopilot-Konto-Wechsel (bfe5876, dazu 73420fe) geprüft – kein
|
||||||
|
App-Code, funktional in Ordnung:** Wechsel-Logik (pyramid↔haupt, `JUST_SWITCHED`
|
||||||
|
verhindert Endlos-Ping-Pong, 20-min-Wartepause wenn beide Konten am Limit),
|
||||||
|
`CLAUDE_CONFIG_DIR` wird vor jeder Session neu gesetzt, Matrix-Token kommt aus
|
||||||
|
`~/gatus/.env` (nicht im Repo). Einzige Anmerkung (belassen): die
|
||||||
|
Limit-Erkennung greppt die GESAMTE Session-Ausgabe auf „usage limit" u. ä. –
|
||||||
|
zitiert eine Session diese englischen Phrasen wörtlich, gibt es einen
|
||||||
|
unnötigen (aber harmlosen) Konto-Wechsel.
|
||||||
|
|
||||||
|
- [x] **(h) AuthLog-Tests (e0ac5cb) geprüft – korrekt, keine Änderung nötig.**
|
||||||
|
Alle 4 Tests gegen `core/auth_log.dart` nachvollzogen: Der Kaputt-Plattform-
|
||||||
|
Test läuft korrekt VOR den Tests mit funktionierender Plattform (das statische
|
||||||
|
`_file`-Handle wird beim Throw nie befüllt, daher kein vergifteter Cache);
|
||||||
|
Kappungs-Test rechnet richtig (521. Zeile triggert Kappung auf 500, „Hallo
|
||||||
|
Log" aus dem Vortest fliegt raus). pubspec-Ergänzung sauber (offizielles
|
||||||
|
`path_provider_platform_interface`, war transitiv im Lockfile). Unabhängig
|
||||||
|
verifiziert: `flutter test` 22/22 grün, `flutter analyze` 1 bekannter
|
||||||
|
Hinweis. Anmerkung (belassen): Die Kappung schreibt die Datei nicht-atomar
|
||||||
|
neu und ist nicht gegen parallele Schreiber aus dem Push-Isolate gesperrt –
|
||||||
|
für ein reines Diagnose-Log akzeptabel, kein Fix nötig.
|
||||||
|
- [x] **(i) settings_prefs-Tests (7dfdbf1) geprüft – Tests korrekt, aber beim
|
||||||
|
Gegenlesen des Produktivcodes 1 echter Befund gefunden und gefixt (7b90f20):
|
||||||
|
Lade-Race in `BoolPref`/`StringSetPref`.** `StringSetPref.add/remove` machten
|
||||||
|
Read-Modify-Write auf möglicherweise noch ungeladenem State – ein `add()`
|
||||||
|
direkt nach Erstellung persistierte nur den neuen Eintrag und LÖSCHTE alle
|
||||||
|
bereits gespeicherten (per neuem Regressionstest am alten Code bewiesen:
|
||||||
|
`['!a:hs']` im Store, `add('!b:hs')` → Store enthielt nur noch `['!b:hs']`).
|
||||||
|
Fix: `add`/`remove` warten jetzt auf `_loaded`. `BoolPref` zusätzlich mit
|
||||||
|
`_userSet`-Guard, damit ein spät landender persistierter Wert nie eine schon
|
||||||
|
getätigte Nutzeraktion zurücküberschreibt (die Microtask-Reihenfolge ist
|
||||||
|
plattform-/versionsabhängig – im Test-Mock zufällig gutartig). Einordnung:
|
||||||
|
in der heutigen App kaum treffbar (die Provider werden beim App-Start geladen,
|
||||||
|
lange bevor jemand klickt – deshalb nie aufgefallen), aber die Fassade wird
|
||||||
|
laut `docs/M2_MODULE_SCHNITT.md` zur Pflicht-Schnittstelle für neue, ggf.
|
||||||
|
früh laufende Aufrufer. 2 neue Regressionstests, alle 24 grün auf dem Pi.
|
||||||
|
- [x] **(j) PC-Testplan-Update (03b84e0) geprüft – stimmt.** Teststand „22
|
||||||
|
Tests" war korrekt (durch (i) jetzt 24 – im Testplan nachgezogen, siehe
|
||||||
|
Eintrag oben).
|
||||||
|
- [x] **(k) Secret-Scan über das ganze Repo – ECHTER Sicherheitsbefund
|
||||||
|
(2026-07-04).** Vier aktive Zugangsdaten im öffentlich erreichbaren Repo
|
||||||
|
(Matrix-Admin-Token doppelt – auch im Client!, Gitea-Release-Token,
|
||||||
|
LiveKit-`apiSecret`, Giphy-Key). Gefahrlose Stellen sofort bereinigt
|
||||||
|
(release.ps1 → Env-Var, Admin-Doku → Platzhalter); Rotation aller Secrets +
|
||||||
|
server-seitiges LiveKit-Token-Minting + nicht-Admin-Diagnose-Token als neue
|
||||||
|
M0-Punkte aufgenommen (brauchen Bernd/Server-Zugriff). Details im
|
||||||
|
PROGRESS-Eintrag 2026-07-04 oben.
|
||||||
|
- [x] **(l) Admin-Token-Entkopplung (925aafb) geprüft – Client-Seite korrekt,
|
||||||
|
am Server-Gegenpart 1 echter Befund gefunden und gefixt (2026-07-04).**
|
||||||
|
Client-Diff verifiziert: neuer `pyr-diag-…`-Token stimmt mit `DIAG_TOKEN` in
|
||||||
|
`server.py` überein, `grep J5lax lib/ scripts/` unabhängig nachgeprüft (leer;
|
||||||
|
einziges verbliebenes Client-Secret ist das LiveKit-`apiSecret`, eigener
|
||||||
|
M0-Punkt). Befund: `/api/e2ee-diagnostics` hatte **kein Body-/Log-Größenlimit**
|
||||||
|
bei öffentlich bekanntem Token → Disk-Fill-DoS möglich. Gefixt + headless
|
||||||
|
verifiziert (200/403/413, Kappung), Details im PROGRESS-Eintrag 2026-07-04.
|
||||||
|
- [x] **(m) LiveKit-Migrationsplan (78e4174) gegen Code + Pi-Konfig geprüft –
|
||||||
|
Behauptungen stimmen.** `livekit_token.dart:6` enthält das `apiSecret`,
|
||||||
|
Aufrufstelle `livekit_call_manager.dart:154` mit `ttlSeconds: 21600`, Grants
|
||||||
|
im Plan identisch mit dem Dart-Code (roomJoin/room/canPublish/canSubscribe/
|
||||||
|
canPublishData, metadata = Anzeigename, iss = LKMatrixPi, sub = identity),
|
||||||
|
`livekit.yaml` `keys:` wie beschrieben, Continuwuity antwortet auf
|
||||||
|
`127.0.0.1:6167` (whoami-Basis). Python-Snippet (stdlib-HS256) fachlich
|
||||||
|
korrekt (base64url ohne Padding, kompaktes JSON, HMAC-SHA256). Anmerkung:
|
||||||
|
Der Plan setzt zusätzlich `nbf`/`name` – LiveKit akzeptiert beides, harmlos.
|
||||||
|
- [x] **(n) LiveKit-Token-Route ist inzwischen LIVE auf dem Pi gebaut (bisher
|
||||||
|
unprotokolliert) – geprüft und funktionsfähig, aber Client noch nicht
|
||||||
|
umgestellt (2026-07-04).** Beim Review von `server.py` gefunden: Eine
|
||||||
|
Vorsession hat die in `docs/LIVEKIT_TOKEN_MIGRATION.md` geplante Route
|
||||||
|
`POST /api/livekit-token` bereits in `server.py` implementiert (whoami-Auth,
|
||||||
|
stdlib-HS256-Minting aus `livekit.yaml`) – der CHANGES-Hook zeigt drei
|
||||||
|
server.py-Edits um 04:33, Dienst-Neustart 04:34, aber kein PROGRESS-Eintrag
|
||||||
|
(Session vor dem Protokoll abgebrochen). Headless verifiziert (127.0.0.1:8080
|
||||||
|
UND öffentlich): ungültiger Matrix-Token → 401, fehlende Felder → 400, 5-KB-Body
|
||||||
|
→ 413 (4-KB-Limit), gültiger Admin-Token → 200; das geminete JWT unabhängig
|
||||||
|
dekodiert: iss=LKMatrixPi, sub/name/metadata = geprüfte user_id (kein Spoofing,
|
||||||
|
Client kann Identität NICHT wählen), video-Grants wie im Client, exp−nbf =
|
||||||
|
21600 s, **Signatur unabhängig gegen `livekit.yaml` gegengerechnet: gültig**.
|
||||||
|
Die Route ist gefahrlos additiv (altes Client-Secret bleibt bis zur Rotation
|
||||||
|
gültig, nichts bricht). **Offen bleibt:** Client-Umstellung
|
||||||
|
(`livekit_token.dart`/`livekit_call_manager.dart`) + Secret-Rotation – beides
|
||||||
|
weiterhin PC/Gerät (heiliger Call-Pfad), siehe Migrationsplan. Server-Seite des
|
||||||
|
M0-LiveKit-Punkts ist damit erledigt.
|
||||||
|
- [x] **(o) KRITISCHER Befund beim server.py-Review: die Admin-Endpoints des
|
||||||
|
Dashboards haben KEINE Authentifizierung und sind öffentlich erreichbar
|
||||||
|
(2026-07-04).** Der Review von (l) hatte nur das Diagnose-Endpoint auf
|
||||||
|
Disk-Fill gehärtet – der viel größere Nachbar blieb unbemerkt:
|
||||||
|
`https://dashboard.steggi-matrix.work` ist über Cloudflare offen aus dem
|
||||||
|
Internet erreichbar, und `POST /api/ban`, `/api/unban`, `/api/toggle-registration`,
|
||||||
|
`/api/create-invite`, `/api/run-stats` prüfen **keinen** Token. Belegt:
|
||||||
|
`curl -X POST .../api/ban -d '{"user":""}'` liefert die App-eigene 400-Antwort
|
||||||
|
`{"error":"Kein Nutzer angegeben"}` – identisch zu 127.0.0.1, also KEIN
|
||||||
|
Cloudflare-Login davor. Damit könnte jeder Fremde Uta (jeden Nutzer) sperren
|
||||||
|
oder die offene Registrierung am Homeserver aktivieren. Der Hostname ist über
|
||||||
|
Certificate-Transparency-Logs praktisch auffindbar – „geheim" trägt nicht.
|
||||||
|
**Bewusst NICHT blind gefixt:** Jede Auth-Ergänzung macht Bernds
|
||||||
|
Browser-Dashboard funktionslos, bis er einen Token einträgt / Cloudflare Access
|
||||||
|
einrichtet – das mitten in einer autonomen Session zu deployen sperrt ihn ohne
|
||||||
|
Vorwarnung aus seinem eigenen Admin-Panel („kein Aussperren"). Fertiger,
|
||||||
|
gegen den echten Code geschriebener Plan mit zwei Wegen (A: Cloudflare Access =
|
||||||
|
Königsweg, kein Code; B: eigener `DASH_TOKEN` + `stats.html`-Prompt) liegt in
|
||||||
|
`docs/DASHBOARD_AUTH_HARDENING.md`. **Braucht Bernds Wahl A oder B, dann
|
||||||
|
sofortige Umsetzung + headless-Verifikation.** In ROADMAP M0 als dringender
|
||||||
|
Punkt aufgenommen. **Update (später am 2026-07-04):** Das akute Loch ist mit
|
||||||
|
einem aussperr-sicheren Interims-Gate geschlossen (Admin-Routen nur noch
|
||||||
|
Heimnetz/localhost, siehe PROGRESS-Eintrag „Dashboard-Befund (o)" oben) –
|
||||||
|
offen bleibt nur noch die Fernzugriffs-Entscheidung A/B.
|
||||||
|
- [x] **(p) Interims-Gate (e97a748/455742d) reviewt – korrekt; dabei halbfertige,
|
||||||
|
unprotokollierte Threading-Härtung einer abgebrochenen Session gefunden und
|
||||||
|
vollendet (2026-07-04).** Gate-Logik bestätigt: Socket-IP ist die belastbare
|
||||||
|
Prüfung (nicht spoofbar), Cf-Header nur Zweitschranke, Deny-Log mit 5-MB-Deckel
|
||||||
|
und ohne attacker-kontrollierten Text; Restrisiken (LAN-CSRF, offene
|
||||||
|
Nutzerlisten-Ansicht) waren bereits in `docs/DASHBOARD_AUTH_HARDENING.md`
|
||||||
|
dokumentiert – kein neuer Befund am Gate. ABER: CHANGES-Hook zeigte drei
|
||||||
|
server.py-Edits um 09:43 ohne Commit/Protokoll – abgebrochene Session hatte
|
||||||
|
`DIAG_LOCK` definiert, aber nie benutzt, und `ThreadingHTTPServer` im Kommentar
|
||||||
|
behauptet, aber nicht umgestellt; der Dienst lief zudem noch mit altem Code.
|
||||||
|
Vollendet (Threading + Lock-Nutzung), Dienst neu gestartet, 11 Headless-
|
||||||
|
Prüfungen grün inkl. Slow-Loris (parallele Anfrage 10 ms statt blockiert;
|
||||||
|
hängender Body-Read nach 30 s gekappt). Details im PROGRESS-Eintrag „(p)" oben.
|
||||||
|
- [x] **(q) Threading-Umstellung aus (p) tiefer geprüft – echte Nebenläufigkeits-Race
|
||||||
|
in den Admin-Routen gefunden und mit `ADMIN_LOCK` behoben (2026-07-04).** (p) hatte
|
||||||
|
den öffentlich beschreibbaren Diagnose-Pfad per `DIAG_LOCK` abgesichert, aber die
|
||||||
|
Admin-Routen übersehen: `toggle-registration`/`create-invite` machen ein
|
||||||
|
Read-Modify-Write auf `conduit.toml` + `docker restart`, und `send_admin()` liest
|
||||||
|
nach 4 s die letzten Admin-Raum-Nachrichten zurück – beides seit multithreaded
|
||||||
|
race-fähig (lost update, doppelter Neustart, vertauschte Antworten). Fix: neues
|
||||||
|
`ADMIN_LOCK`, das **nur** die Admin-Mutationen serialisiert; die öffentlichen Routen
|
||||||
|
(`livekit-token`, `e2ee-diagnostics`) laufen zuerst und bleiben nebenläufig
|
||||||
|
(Slow-Loris-Schutz aus (p) unangetastet). Handler-Logik wortwörtlich unverändert
|
||||||
|
(per `diff` gegen Backup verifiziert). Headless verifiziert: 8 Routen-Checks wie
|
||||||
|
vorher; Race-Beweis (2 parallele `run-stats` 2633 ms ≈ 2× einzeln 1388 ms →
|
||||||
|
serialisiert); Non-Blocking-Beweis (öffentliches `livekit-token` während gehaltenem
|
||||||
|
Lock in 11 ms). Details im PROGRESS-Eintrag „(q)" oben.
|
||||||
|
- [x] **(r) ADMIN_LOCK-Umbau aus (q) (c8ff850) geprüft – Umbau korrekt, 3 kleinere
|
||||||
|
Restbefunde gefixt (2026-07-04).** Locking-Struktur und Routen-Reihenfolge von (q)
|
||||||
|
bestätigt (öffentliche Routen vor dem Gate/Lock, Handler-Logik unverändert).
|
||||||
|
Befunde: (1) `GET /api/registration-status` las `conduit.toml` ohne Lock, während
|
||||||
|
die Admin-Routen sie per Truncate+Write neu schreiben – Leser konnte seit
|
||||||
|
multithreaded (p) eine halb geschriebene Datei sehen; Fix: Lesen unter `ADMIN_LOCK`
|
||||||
|
(bewusst KEIN `os.replace`: die Datei ist als Einzeldatei in den Container
|
||||||
|
gemountet, Inode-Falle – im Code dokumentiert). (2) Negative `Content-Length`
|
||||||
|
ließ `read()` bis EOF blockieren (Thread hing bis zum 30-s-Timeout) – bei
|
||||||
|
`livekit-token`/`e2ee-diagnostics`/`ban`/`unban` wird `< 0` jetzt mit 413
|
||||||
|
abgelehnt. (3) `ban`/`unban` ohne Body-Limit (hielten dabei den ADMIN_LOCK) →
|
||||||
|
4-KB-Deckel; Einladungs-Token von `random` auf `secrets` umgestellt (Token
|
||||||
|
schaltet Registrierung frei). Headless verifiziert (8 Checks inkl. 8-KB-Body →
|
||||||
|
413, `Content-Length: -5` → sofort 413 statt Hänger). Details im
|
||||||
|
PROGRESS-Eintrag „(r)" oben.
|
||||||
|
- [x] **(s) (r)-Fixes (5ada0e5) geprüft – korrekt, 2 kleinere Restbefunde gefixt
|
||||||
|
(2026-07-04).** (r)-Diff gegen Backup verifiziert (GET-Leser-Lock,
|
||||||
|
Content-Length-Härtung, secrets-Token – alles wie protokolliert), Dienst lief
|
||||||
|
mit aktuellem Code, kein unprotokollierter Rest im CHANGES-Hook-Log. Befunde
|
||||||
|
beim Ganzdatei-Gegenlesen: (1) ban/unban las den Body innerhalb des
|
||||||
|
`ADMIN_LOCK` – ein Client mit angekündigtem, nie gesendetem Body hielt den
|
||||||
|
Lock bis zum 30-s-Socket-Timeout (das 4-KB-Limit deckelt nur die Größe, nicht
|
||||||
|
die Wartezeit; seit (r) hing damit auch der `registration-status`-Leser mit);
|
||||||
|
Fix: Body-Lesen/Validieren vor den Lock gezogen, Handler-Logik unverändert.
|
||||||
|
(2) `livekit-token`/`e2ee-diagnostics` leakten bei Fehlern `str(e)` an
|
||||||
|
Unauthentifizierte (interne Pfade/Adressen); Fix: generisches
|
||||||
|
`{"error": "Interner Fehler"}`, Details ins Journal (stderr). Headless
|
||||||
|
verifiziert: 8 Routen-Checks unverändert, Lock-Beweis (hängender ban-Body,
|
||||||
|
paralleler registration-status-GET in 15 ms), 500-Beweis (ungültiges JSON →
|
||||||
|
generisch, Detail im Journal). Details im PROGRESS-Eintrag „(s)" oben.
|
||||||
|
- [x] **(t) (s)-Fixes (13b333c) geprüft – korrekt, 1 kleiner Restbefund gefixt
|
||||||
|
(2026-07-04).** (s)-Diff gegen Backup verifiziert (ban/unban-Body vor dem
|
||||||
|
Lock inkl. Validierung, generische 500er auf den öffentlichen POST-Routen –
|
||||||
|
alles wie protokolliert), Dienst lief mit aktuellem Code, kein
|
||||||
|
unprotokollierter Rest im CHANGES-Hook-Log. Befund beim
|
||||||
|
Ganzdatei-Gegenlesen: `GET /api/registration-status` (öffentlich, `do_GET`
|
||||||
|
hat kein Gate) antwortete bei Fehlern noch mit `str(e)` – hätte z. B. den
|
||||||
|
`conduit.toml`-Pfad geleakt; (s) hatte nur die POST-Routen gefixt. Fix:
|
||||||
|
generisches `{"error": "Interner Fehler"}`, Detail ins Journal. Headless
|
||||||
|
verifiziert: 8 Routen-Checks unverändert, 500-Beweis per kurzzeitigem
|
||||||
|
`chmod 000` auf `conduit.toml` (generisch nach außen, `PermissionError` im
|
||||||
|
Journal, danach wieder 200). Details im PROGRESS-Eintrag „(t)" oben.
|
||||||
|
- [x] **(u) (t)-Fix (d3e75c3) geprüft – korrekt, KEIN Befund; server.py-Kette
|
||||||
|
abgeschlossen (2026-07-04).** Diff gegen `server.py.bak-20260704-review-t`
|
||||||
|
= exakt der protokollierte Mini-Fix, Dienst läuft mit aktuellem Code, kein
|
||||||
|
unprotokollierter Rest. Verbliebene `str(e)`-Stellen alle LAN-gegated
|
||||||
|
(absichtlich). Ohne neuen Anlass keine weitere Review-Runde an `server.py`.
|
||||||
|
Details im PROGRESS-Eintrag „(u)" oben.
|
||||||
|
- [x] **(v) Entscheidungs-Doku 825481e (`ANTWORTEN_BERND.md`, am PC entstanden)
|
||||||
|
geprüft – konsistent, 1 Doku-Lücke gefunden und behoben (2026-07-05).** Alle
|
||||||
|
referenzierten Pläne/Dateien existieren, die vier Entscheidungen decken die
|
||||||
|
gesammelten „Fragen an Bernd" ab. Befund: Entscheidung 3 (Dashboard
|
||||||
|
Heimnetz-only, A/B geschlossen) war nicht in ROADMAP.md /
|
||||||
|
`docs/DASHBOARD_AUTH_HARDENING.md` nachgezogen – beide stellten die Frage
|
||||||
|
weiter als offen dar. In derselben Session behoben (ROADMAP-Punkt
|
||||||
|
geschlossen, Doku-Status aktualisiert, Vermerk zu Entscheidung 4 am
|
||||||
|
Secrets-Punkt). Details im PROGRESS-Eintrag „(v)+(w)" oben.
|
||||||
|
- [x] **(w) autopilot.sh-Fix 4477ed9 (CLAUDE_CONFIG_DIR unset für Hauptkonto,
|
||||||
|
am PC entstanden) geprüft – korrekt, kein Befund (2026-07-05).** Begründung
|
||||||
|
im Commit stimmt (CLI sucht Config unter `$CLAUDE_CONFIG_DIR/.claude.json`,
|
||||||
|
Hauptprofil liegt unter `~/.claude.json`); `unset` wirkt auch nach
|
||||||
|
pyramid-Iterationen derselben Shell. Am lebenden System verifiziert: die
|
||||||
|
Review-Session selbst lief als Konto „haupt" mit leerem `CLAUDE_CONFIG_DIR`
|
||||||
|
und korrekt geladenem Profil. Kein weiterer `CLAUDE_CONFIG_DIR`-Verweis im
|
||||||
|
Repo. Details im PROGRESS-Eintrag „(v)+(w)" oben.
|
||||||
|
|
||||||
**Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken
|
**Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken
|
||||||
(verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene
|
(verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene
|
||||||
Fable-5-Vorsession bereits gefixt – beide Fixes heute unabhängig verifiziert.
|
Fable-5-Vorsession bereits gefixt – beide Fixes heute unabhängig verifiziert.
|
||||||
|
|||||||
+76
@@ -60,6 +60,82 @@ Punkte abhaken (`[x]`), wenn erledigt UND in `PROGRESS.md` protokolliert.
|
|||||||
- [x] Geräte-/Sessionverwaltung in den Einstellungen: bereits vorhanden
|
- [x] Geräte-/Sessionverwaltung in den Einstellungen: bereits vorhanden
|
||||||
(`_SessionsSection` in `settings_modal.dart` – Liste, Umbenennen, Verifizieren
|
(`_SessionsSection` in `settings_modal.dart` – Liste, Umbenennen, Verifizieren
|
||||||
per SAS/QR, Abmelden, Massen-Abmeldung mit Passwort-Reauth)
|
per SAS/QR, Abmelden, Massen-Abmeldung mit Passwort-Reauth)
|
||||||
|
- [x] **SICHERHEIT: Dashboard-Admin-Endpoints – ERLEDIGT: Loch geschlossen,
|
||||||
|
Heimnetz-only ist laut Bernd der Endzustand** (Fable-5-Review (o);
|
||||||
|
Entscheidung `ANTWORTEN_BERND.md` Nr. 3, 2026-07-05).
|
||||||
|
`server.py` nimmt `/api/ban`, `/api/unban`, `/api/toggle-registration`,
|
||||||
|
`/api/create-invite`, `/api/run-stats` jetzt NUR noch aus Heimnetz/localhost an
|
||||||
|
(Socket-IP + Cloudflare-Header-Check); Fremde aus dem Internet bekommen 403.
|
||||||
|
Headless verifiziert (öffentlich 403, Heimnetz ok, App-Endpoints unverändert),
|
||||||
|
siehe PROGRESS.md 2026-07-04. **Bernds Dashboard läuft im Heimnetz unverändert:
|
||||||
|
`http://192.168.178.71:8080/stats.html`.** Offen bleibt: Will Bernd die
|
||||||
|
Admin-Buttons auch von unterwegs? Dann Weg A (Cloudflare Access, empfohlen –
|
||||||
|
schließt auch die weiterhin öffentliche Nutzerlisten-Ansicht) oder Weg B
|
||||||
|
(`DASH_TOKEN`), Plan + Restrisiken in `docs/DASHBOARD_AUTH_HARDENING.md`.
|
||||||
|
Zusatz (Fable-5-Review (p), 2026-07-04): `server.py` läuft jetzt multithreaded
|
||||||
|
mit 30-s-Socket-Timeout – ein einzelner langsamer Client kann den Server
|
||||||
|
(und damit `/api/livekit-token`) nicht mehr blockieren; headless verifiziert.
|
||||||
|
Zusatz (Fable-5-Review (q), 2026-07-04): Der durch (p) eingeführte
|
||||||
|
Nebenläufigkeits-Race in den Admin-Routen (Read-Modify-Write auf `conduit.toml`,
|
||||||
|
`send_admin`-Readback) ist mit `ADMIN_LOCK` geschlossen, das NUR die
|
||||||
|
Admin-Mutationen serialisiert – die öffentlichen Routen bleiben nebenläufig;
|
||||||
|
headless verifiziert (2× parallel = 2× Laufzeit, öffentlicher Pfad 11 ms).
|
||||||
|
Zusatz (Fable-5-Review (r), 2026-07-04): drei Restbefunde gefixt –
|
||||||
|
`registration-status`-Leser jetzt unter ADMIN_LOCK (Truncate-Write-Race),
|
||||||
|
negative/übergroße `Content-Length` überall abgelehnt (kein Thread-Hänger,
|
||||||
|
ban/unban 4-KB-Deckel), Einladungs-Token per `secrets` statt `random`.
|
||||||
|
Zusatz (Fable-5-Review (s), 2026-07-04): ban/unban-Body wird jetzt VOR dem
|
||||||
|
ADMIN_LOCK gelesen (hängender Body-Read konnte den Lock 30 s festhalten),
|
||||||
|
und die öffentlichen Routen antworten bei internen Fehlern generisch statt
|
||||||
|
mit Exception-Text (Info-Leak); Details ins Journal. Headless verifiziert.
|
||||||
|
Zusatz (Fable-5-Review (t), 2026-07-04): auch der öffentliche GET-Leser
|
||||||
|
`registration-status` antwortet bei internen Fehlern jetzt generisch
|
||||||
|
statt mit `str(e)` (hätte den `conduit.toml`-Pfad geleakt); headless
|
||||||
|
verifiziert inkl. 500-Beweis.
|
||||||
|
Zusatz (Fable-5-Review (u), 2026-07-04): (t)-Fix gegengelesen – korrekt,
|
||||||
|
kein Befund; die server.py-Härtungskette ist damit KONVERGIERT (alle
|
||||||
|
verbliebenen `str(e)`-Antworten LAN-gegated, absichtlich).
|
||||||
|
**Abschluss (2026-07-05, `ANTWORTEN_BERND.md` Nr. 3): KEIN Fernzugriff
|
||||||
|
gewünscht – die A/B-Frage ist GESCHLOSSEN, das Heimnetz-Gate ist der
|
||||||
|
Endzustand (unterwegs nutzt Bernd WireGuard). Die Restrisiken des Gates
|
||||||
|
(öffentliche Nutzerlisten-Ansicht, theoretisches LAN-CSRF) sind bewusst
|
||||||
|
akzeptiert und in `docs/DASHBOARD_AUTH_HARDENING.md` dokumentiert.
|
||||||
|
Nicht weiter daran arbeiten.**
|
||||||
|
- [x] **LiveKit-Token-Route server-seitig gebaut + verifiziert** (Fable-5-Review (n),
|
||||||
|
PROGRESS.md 2026-07-04). `POST /api/livekit-token` läuft live in `server.py`
|
||||||
|
(whoami-Auth, stdlib-HS256-Minting aus `livekit.yaml`), headless geprüft
|
||||||
|
(401/400/413/200, JWT-Signatur unabhängig gegen `livekit.yaml` gültig, Identität =
|
||||||
|
geprüfte user_id). **Offen:** Client-Umstellung (`livekit_token.dart`/
|
||||||
|
`livekit_call_manager.dart`) + Secret-Rotation – beides PC/Gerät (heiliger
|
||||||
|
Call-Pfad), siehe `docs/LIVEKIT_TOKEN_MIGRATION.md`.
|
||||||
|
- [ ] **SICHERHEIT: geleakte Secrets rotieren + aus dem Client holen** (Fable-5-Review (k),
|
||||||
|
PROGRESS.md 2026-07-04). Das Gitea-Repo ist öffentlich (`private:false`), und es lagen
|
||||||
|
vier aktive Secrets drin – teils schon bereinigt (release.ps1, Admin-Doku), aber:
|
||||||
|
- **Rotieren (Pflicht, sie sind bereits geleakt):** Matrix-Server-Admin-Token
|
||||||
|
`J5lax…` (noch gültig, voller Admin!), Gitea-Release-Token, LiveKit-`apiKey`/
|
||||||
|
`apiSecret`, Giphy-Key. Alte jeweils widerrufen.
|
||||||
|
- **LiveKit-Token server-seitig minten:** kleiner Token-Endpoint auf dem Pi, damit
|
||||||
|
`apiSecret` aus `lib/core/livekit_token.dart` verschwindet (App holt nur das JWT).
|
||||||
|
**Umsetzungsplan liegt fertig in `docs/LIVEKIT_TOKEN_MIGRATION.md`** (2026-07-04,
|
||||||
|
gegen Code + Pi-Konfig geschrieben) – braucht echten Call-Test auf einem Gerät.
|
||||||
|
- **E2EE-Diagnose-Upload: ERLEDIGT (2026-07-04).** Der Dashboard-Server auf dem Pi
|
||||||
|
(`/home/steggi/matrix/server.py`) benutzte für `/api/e2ee-diagnostics` denselben
|
||||||
|
String wie den Matrix-Admin-Token – deshalb steckte er im Client. Jetzt hat das
|
||||||
|
Diagnose-Endpoint einen eigenen, eng begrenzten Token (`DIAG_TOKEN`, hängt nur an
|
||||||
|
den Log an, keine Admin-Rechte); `settings_encryption.dart` benutzt diesen. Der
|
||||||
|
**Admin-Token ist damit nicht mehr im App-Code** (per `grep` bestätigt). Server +
|
||||||
|
Client umgestellt und verifiziert (neuer Token 200, alter Admin-Token 403). Der
|
||||||
|
Admin-Token selbst muss trotzdem noch rotiert werden (steht oben, Bernds Sache).
|
||||||
|
- Optional: Git-History-Rewrite (Bernds Entscheidung, Repo ist auch Backup).
|
||||||
|
- **Stand 2026-07-05 (`ANTWORTEN_BERND.md` Nr. 4): Rotation und
|
||||||
|
History-Rewrite ruhen BEWUSST – beides bleibt Bernds manuelle Sache,
|
||||||
|
NICHT vom Autopilot anstoßen oder nachfragen. Der Punkt bleibt nur als
|
||||||
|
Merkposten offen; Autopilot-bearbeitbar ist hier nichts mehr.**
|
||||||
|
- **ACHTUNG bei „Repo einfach privat schalten":** Der In-App-Updater
|
||||||
|
(`lib/core/update_checker.dart`) holt Releases **anonym** über die öffentliche
|
||||||
|
Gitea-API (`/api/v1/repos/steggi/pyramid/releases`). Repo privat = Utas App
|
||||||
|
findet keine Updates mehr (und ein Fix ließe sich nicht mehr per Update
|
||||||
|
verteilen – Henne-Ei). Erst Updater-Strategie klären, dann Sichtbarkeit ändern.
|
||||||
- [ ] Härtetest dokumentieren: Login → Nachrichten → Logout → Login neu → alles noch lesbar
|
- [ ] Härtetest dokumentieren: Login → Nachrichten → Logout → Login neu → alles noch lesbar
|
||||||
(gehört zum ausstehenden PC-Praxistest der beiden Bugfixes oben;
|
(gehört zum ausstehenden PC-Praxistest der beiden Bugfixes oben;
|
||||||
kompletter Ablauf inkl. aller aufgelaufenen UNGETESTET-Punkte steht
|
kompletter Ablauf inkl. aller aufgelaufenen UNGETESTET-Punkte steht
|
||||||
|
|||||||
+46
-5
@@ -9,9 +9,32 @@
|
|||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
export PATH="$HOME/.local/bin:$HOME/flutter/bin:$PATH"
|
export PATH="$HOME/.local/bin:$HOME/flutter/bin:$PATH"
|
||||||
|
|
||||||
# Pyramid laeuft auf eigenem Claude-Konto (Profil ~/.claude-pyramid),
|
# --- Zwei Claude-Konten mit automatischem Wechsel bei 5h-Limit ---
|
||||||
# das Standard-Profil ~/.claude bleibt fuer die Webseiten-Arbeit.
|
# Konto "pyramid" = Profil ~/.claude-pyramid (Standard fuer diese Arbeit),
|
||||||
export CLAUDE_CONFIG_DIR="$HOME/.claude-pyramid"
|
# Konto "haupt" = Profil ~/.claude (springt ein, wenn pyramid am Limit ist).
|
||||||
|
ACCOUNT_FILE="$HOME/.claude-accounts/active"
|
||||||
|
get_account() { cat "$ACCOUNT_FILE" 2>/dev/null || echo pyramid; }
|
||||||
|
|
||||||
|
apply_account() {
|
||||||
|
if [ "$(get_account)" = "haupt" ]; then
|
||||||
|
# Hauptkonto = Standard-Profil -> Variable NICHT setzen
|
||||||
|
# (sonst sucht Claude die Config unter ~/.claude/.claude.json statt ~/.claude.json)
|
||||||
|
unset CLAUDE_CONFIG_DIR
|
||||||
|
else
|
||||||
|
export CLAUDE_CONFIG_DIR="$HOME/.claude-pyramid"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_account() {
|
||||||
|
local cur next
|
||||||
|
cur=$(get_account)
|
||||||
|
if [ "$cur" = "pyramid" ]; then next=haupt; else next=pyramid; fi
|
||||||
|
mkdir -p "$HOME/.claude-accounts"
|
||||||
|
echo "$next" > "$ACCOUNT_FILE"
|
||||||
|
notify_matrix "🔺 Autopilot: 5h-Limit auf Konto '$cur' erreicht – wechsle auf Konto '$next' und arbeite direkt weiter."
|
||||||
|
}
|
||||||
|
|
||||||
|
JUST_SWITCHED=0
|
||||||
|
|
||||||
# --- Matrix-Statusberichte (Raum "🔺 Pyramid Autopilot", Token vom Gatus-Bot) ---
|
# --- Matrix-Statusberichte (Raum "🔺 Pyramid Autopilot", Token vom Gatus-Bot) ---
|
||||||
MATRIX_URL="http://127.0.0.1:6167"
|
MATRIX_URL="http://127.0.0.1:6167"
|
||||||
@@ -36,9 +59,11 @@ while true; do
|
|||||||
sleep 600
|
sleep 600
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
echo "===== $(date '+%F %T') Neue Claude-Session =====" | tee -a autopilot.log
|
apply_account
|
||||||
|
echo "===== $(date '+%F %T') Neue Claude-Session (Konto $(get_account)) =====" | tee -a autopilot.log
|
||||||
BEFORE=$(git rev-parse HEAD 2>/dev/null)
|
BEFORE=$(git rev-parse HEAD 2>/dev/null)
|
||||||
claude -p "$PROMPT" --model claude-fable-5 --dangerously-skip-permissions 2>&1 | tee -a autopilot.log
|
SESSION_OUT=$(mktemp)
|
||||||
|
claude -p "$PROMPT" --model claude-fable-5 --dangerously-skip-permissions 2>&1 | tee -a autopilot.log "$SESSION_OUT"
|
||||||
AFTER=$(git rev-parse HEAD 2>/dev/null)
|
AFTER=$(git rev-parse HEAD 2>/dev/null)
|
||||||
if [ -n "$BEFORE" ] && [ "$BEFORE" != "$AFTER" ]; then
|
if [ -n "$BEFORE" ] && [ "$BEFORE" != "$AFTER" ]; then
|
||||||
COMMITS=$(git log --oneline --no-decorate "$BEFORE..$AFTER" | head -15)
|
COMMITS=$(git log --oneline --no-decorate "$BEFORE..$AFTER" | head -15)
|
||||||
@@ -50,6 +75,22 @@ $COMMITS
|
|||||||
|
|
||||||
$NEXT"
|
$NEXT"
|
||||||
fi
|
fi
|
||||||
|
# 5h-Limit erkannt? -> sofort aufs andere Konto wechseln statt zu warten
|
||||||
|
if grep -qiE "hit your.*limit|usage limit|session limit" "$SESSION_OUT"; then
|
||||||
|
rm -f "$SESSION_OUT"
|
||||||
|
if [ "$JUST_SWITCHED" != "1" ]; then
|
||||||
|
switch_account
|
||||||
|
JUST_SWITCHED=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
JUST_SWITCHED=0
|
||||||
|
echo "===== $(date '+%F %T') Beide Konten am Limit - naechster Versuch in 20 Minuten =====" | tee -a autopilot.log
|
||||||
|
sleep 1200
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
rm -f "$SESSION_OUT"
|
||||||
|
JUST_SWITCHED=0
|
||||||
|
|
||||||
echo "===== $(date '+%F %T') Session beendet - Neustart in 20 Minuten (Strg+C zum Stoppen) =====" | tee -a autopilot.log
|
echo "===== $(date '+%F %T') Session beendet - Neustart in 20 Minuten (Strg+C zum Stoppen) =====" | tee -a autopilot.log
|
||||||
sleep 1200
|
sleep 1200
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# Dashboard-Server absichern: Admin-Endpoints haben KEINE Authentifizierung
|
||||||
|
|
||||||
|
**Status (ABGESCHLOSSEN 2026-07-05): Das Loch ist mit dem Heimnetz-Gate
|
||||||
|
(„Weg C", siehe unten) geschlossen, und Bernd hat entschieden
|
||||||
|
(`ANTWORTEN_BERND.md` Nr. 3): KEIN Fernzugriff – das Gate ist der ENDZUSTAND.**
|
||||||
|
Die fünf Admin-Endpoints akzeptieren nur noch Anfragen aus dem
|
||||||
|
Heimnetz/localhost; alles, was über den Cloudflare-Tunnel kommt, bekommt 403 mit
|
||||||
|
einer Erklärung (die das Dashboard per `alert()` anzeigt). Bernds Dashboard
|
||||||
|
funktioniert im Heimnetz unverändert unter
|
||||||
|
`http://192.168.178.71:8080/stats.html`; unterwegs kommt Bernd per WireGuard ins
|
||||||
|
Heimnetz. Die A/B-Frage (Cloudflare Access vs. `DASH_TOKEN`) ist damit
|
||||||
|
GESCHLOSSEN – die Optionen unten bleiben nur als Archiv stehen, falls sich der
|
||||||
|
Bedarf je ändert. Die unten dokumentierten Restrisiken des Gates sind bewusst
|
||||||
|
akzeptiert. **Nicht weiter an diesem Thema arbeiten.**
|
||||||
|
|
||||||
|
## Heimnetz-Gate („Weg C", aktiv seit 2026-07-04, seit 2026-07-05 Endzustand)
|
||||||
|
|
||||||
|
In `server.py` (Konstante `ADMIN_PATHS` + Helfer `lan_denied_reason`): eine
|
||||||
|
Admin-POST-Anfrage wird nur angenommen, wenn (1) **kein** `Cf-Connecting-Ip`/
|
||||||
|
`Cf-Ray`-Header vorhanden ist (= sie kam nicht durch den Cloudflare-Tunnel) und
|
||||||
|
(2) die **Socket-Absender-IP** (nicht spoofbar) `127.0.0.1`/`::1`/`192.168.*`
|
||||||
|
ist. Abgelehnte Versuche landen größenbegrenzt (5 MB-Deckel) in
|
||||||
|
`security_alerts.log` und damit in der Dashboard-Anzeige. Headless verifiziert:
|
||||||
|
öffentliche URL → 403 (ban/toggle), Heimnetz-IP und localhost → App-Validierung
|
||||||
|
erreicht (400 bei leerem Nutzer), gespoofter Cf-Header aus dem LAN → 403,
|
||||||
|
`GET /` öffentlich → 200, `/api/livekit-token` (401 bei ungültigem Token) und
|
||||||
|
`/api/e2ee-diagnostics` (403 bei falschem Token) unverändert öffentlich
|
||||||
|
erreichbar (die App braucht sie, eigene Auth vorhanden).
|
||||||
|
|
||||||
|
**Bekannte Restrisiken des Gates (Bernd bekannt und bewusst akzeptiert,
|
||||||
|
Entscheidung 2026-07-05):**
|
||||||
|
- Die **öffentliche Ansicht bleibt offen** und zeigt Nutzerliste + Präsenz
|
||||||
|
(Informations-Preisgabe). Weg A würde auch das schließen.
|
||||||
|
- **CSRF aus dem Heimnetz:** Eine bösartige Webseite in Bernds Browser könnte
|
||||||
|
theoretisch POSTs an `http://192.168.178.71:8080` schicken (moderne
|
||||||
|
Chrome-Versionen blocken Public→Private-Requests, Firefox nicht vollständig).
|
||||||
|
Erst ein Token/Access-Login (Weg A/B) schließt das sauber.
|
||||||
|
- Wer **im Heimnetz** ist, kann weiterhin ohne Anmeldung administrieren.
|
||||||
|
|
||||||
|
## Problem (belegt, nicht vermutet)
|
||||||
|
|
||||||
|
Der Dashboard-Server `/home/steggi/matrix/server.py` (läuft als
|
||||||
|
`matrix-stats.service` auf `0.0.0.0:8080`) ist über Cloudflare unter
|
||||||
|
`https://dashboard.steggi-matrix.work` **öffentlich aus dem Internet erreichbar**.
|
||||||
|
Getestet: `GET /` liefert 200 (stats.html), und die **zustandsändernden POST-Endpoints
|
||||||
|
antworten mit der App-eigenen Validierung – ohne jede Authentifizierung**:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl -X POST https://dashboard.steggi-matrix.work/api/ban -d '{"user":""}'
|
||||||
|
{"error": "Kein Nutzer angegeben"} # HTTP 400 – App-Antwort, KEIN Login davor
|
||||||
|
```
|
||||||
|
|
||||||
|
Damit kann **jeder Fremde im Internet**, der den Hostnamen kennt, ohne Anmeldung:
|
||||||
|
|
||||||
|
- `/api/ban` / `/api/unban` – **jeden Matrix-Nutzer sperren/entsperren** (auch Uta!)
|
||||||
|
→ Aussperrung / Denial-of-Service gegen echte Nutzer.
|
||||||
|
- `/api/toggle-registration` und `/api/create-invite` – **offene Registrierung am
|
||||||
|
Homeserver aktivieren** (setzt `allow_registration = true` +
|
||||||
|
`yes_i_am_very_very_sure…`) → der Homeserver wird zur Spam-/Missbrauchsschleuder.
|
||||||
|
- `/api/run-stats` – Stats-Skript auslösen (harmlos, aber ebenfalls ungeschützt).
|
||||||
|
|
||||||
|
**Der Hostname ist nicht wirklich geheim:** Cloudflare stellt TLS-Zertifikate aus,
|
||||||
|
die in den öffentlichen Certificate-Transparency-Logs landen – jede Subdomain von
|
||||||
|
`steggi-matrix.work` ist per CT-Log-Scan auffindbar. Die bisherige „Sicherheit"
|
||||||
|
beruht also allein auf Verborgenheit eines Namens, der praktisch entdeckbar ist.
|
||||||
|
|
||||||
|
Bereits **korrekt abgesichert** (zum Vergleich, nicht betroffen):
|
||||||
|
- `/api/e2ee-diagnostics` – eigener `DIAG_TOKEN` (Bearer).
|
||||||
|
- `/api/livekit-token` – Matrix-Access-Token per `whoami` gegen Continuwuity.
|
||||||
|
|
||||||
|
Nur die vier Admin-/Stats-Endpoints oben sind offen.
|
||||||
|
|
||||||
|
## Wie das Dashboard heute aufruft
|
||||||
|
|
||||||
|
`stats.html` ruft alle Aktionen als **Same-Origin-`fetch` ohne Auth-Header** auf
|
||||||
|
(`toggleRegistration`, `createInvite`, `refreshStats`, `doUserAction` →
|
||||||
|
`/api/ban` /`/api/unban`). Es gibt keine Session, kein Cookie, keinen Token –
|
||||||
|
deshalb bricht **jede** hinzugefügte Authentifizierung das Dashboard so lange,
|
||||||
|
bis `stats.html` den Token mitschickt. Genau darum ist das eine Bernd-Entscheidung
|
||||||
|
und kein stiller Fix.
|
||||||
|
|
||||||
|
## Option A (empfohlen, robust): Cloudflare Access vor die Subdomain
|
||||||
|
|
||||||
|
Am saubersten wird das Dashboard **komplett** durch Cloudflare Access geschützt
|
||||||
|
(Zero-Trust-Login vor der Subdomain), statt in `server.py` Auth nachzubauen:
|
||||||
|
|
||||||
|
1. Im Cloudflare-Zero-Trust-Dashboard eine **Access-Application** für
|
||||||
|
`dashboard.steggi-matrix.work` anlegen.
|
||||||
|
2. Policy: nur Bernds E-Mail (`bernd.steckmeister@gmail.com`) per One-Time-PIN
|
||||||
|
oder Google-Login zulassen.
|
||||||
|
3. Fertig – `server.py` und `stats.html` bleiben **unverändert**, das Dashboard
|
||||||
|
funktioniert für Bernd wie bisher (nach dem Access-Login), aber Fremde kommen
|
||||||
|
gar nicht erst an die Endpoints.
|
||||||
|
|
||||||
|
Vorteil: kein Secret im Code, kein Umbau der HTML-Fetches, kein Aussperr-Risiko
|
||||||
|
durch vergessene Tokens. Nachteil: Bernd muss die Access-App einmal im
|
||||||
|
Cloudflare-Panel klicken (kein Code, aber sein Konto nötig). **Das ist der
|
||||||
|
Königsweg – Option B nur, falls Access nicht gewünscht ist.**
|
||||||
|
|
||||||
|
## Option B (Fallback, rein in server.py): eigener Dashboard-Token
|
||||||
|
|
||||||
|
Wenn Access nicht gewünscht ist, ein **neuer, zufälliger `DASH_TOKEN`** (getrennt
|
||||||
|
vom geleakten Admin-Token `J5lax…` und vom `DIAG_TOKEN`!), den die vier
|
||||||
|
Admin-/Stats-Endpoints als `Authorization: Bearer <DASH_TOKEN>` verlangen:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# server.py, zu den anderen Token-Konstanten:
|
||||||
|
DASH_TOKEN = "<neu erzeugen: python3 -c 'import secrets;print(\"dash-\"+secrets.token_urlsafe(24))'>"
|
||||||
|
|
||||||
|
def _dash_authed(self):
|
||||||
|
return self.headers.get("Authorization", "") == "Bearer " + DASH_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
In `do_POST` **vor** der Behandlung von `/api/ban`, `/api/unban`,
|
||||||
|
`/api/toggle-registration`, `/api/create-invite`, `/api/run-stats` einfügen:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if self.path in ("/api/ban", "/api/unban", "/api/toggle-registration",
|
||||||
|
"/api/create-invite", "/api/run-stats") and not self._dash_authed():
|
||||||
|
self.send_error(403)
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /` und `/api/registration-status` bleiben öffentlich (nur Lesen), damit die
|
||||||
|
Stats-Seite ohne Token sichtbar bleibt.
|
||||||
|
|
||||||
|
**`stats.html`** darf den Token **NIEMALS fest eingebettet** ausliefern (die Seite
|
||||||
|
ist öffentlich – der Token läge sonst für jeden offen). Stattdessen einmal im
|
||||||
|
Browser abfragen und in `localStorage` halten:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function dashToken() {
|
||||||
|
var t = localStorage.getItem('dashToken');
|
||||||
|
if (!t) { t = prompt('Dashboard-Token:'); if (t) localStorage.setItem('dashToken', t); }
|
||||||
|
return t || '';
|
||||||
|
}
|
||||||
|
// und bei JEDEM Aktions-fetch:
|
||||||
|
fetch('/api/ban', { method:'POST',
|
||||||
|
headers: {'Content-Type':'application/json', 'Authorization':'Bearer ' + dashToken()},
|
||||||
|
body: JSON.stringify({user:u}) })
|
||||||
|
```
|
||||||
|
|
||||||
|
(betrifft `toggleRegistration`, `createInvite`, `refreshStats`, `doUserAction`).
|
||||||
|
Bei 403 den gecachten Token verwerfen (`localStorage.removeItem('dashToken')`) und
|
||||||
|
neu fragen. Bernd gibt den Token so genau **einmal pro Browser** ein.
|
||||||
|
|
||||||
|
## Rollout / Testplan
|
||||||
|
|
||||||
|
**Option A:** Access-App klicken → von einem fremden Netz/Inkognito prüfen, dass
|
||||||
|
`https://dashboard.steggi-matrix.work/` zum Access-Login umleitet statt die Seite
|
||||||
|
zu zeigen; dann als Bernd einloggen und alle Buttons testen. Kein Code-Deploy.
|
||||||
|
|
||||||
|
**Option B (headless auf dem Pi verifizierbar):**
|
||||||
|
1. `server.py` + `stats.html` ändern, Dienst-Neustart (Prozess killen,
|
||||||
|
`Restart=always` lädt neu – siehe PROGRESS 2026-07-04 zur polkit-Falle).
|
||||||
|
2. `curl -X POST .../api/ban -d '{"user":"x"}'` **ohne** Bearer → **403** erwartet.
|
||||||
|
3. Mit `Authorization: Bearer <DASH_TOKEN>` + leerem Nutzer → 400 (App-Validierung
|
||||||
|
erreicht) → beweist: Token akzeptiert, Auth greift.
|
||||||
|
4. Im Browser: Dashboard öffnen, Token eingeben, je ein Button testen
|
||||||
|
(Registrierung toggeln + sofort zurück, Stats aktualisieren; Ban/Unban an
|
||||||
|
einem **Test-Nutzer**, nicht an Uta).
|
||||||
|
|
||||||
|
## Warum erst nur der Plan kam – und dann doch das Interims-Gate
|
||||||
|
|
||||||
|
Eine Token-/Access-Absicherung (A/B) macht die Dashboard-Buttons überall
|
||||||
|
funktionslos, bis Bernd aktiv etwas tut (Token eingeben bzw. Access-App
|
||||||
|
einrichten) – das mitten in einer autonomen Session zu deployen verletzt die
|
||||||
|
CLAUDE.md-Leitplanke „kein Aussperren". Das *Wie* der Anmeldung bleibt deshalb
|
||||||
|
Bernds Richtungsentscheidung. Das später ergänzte Interims-Gate umgeht dieses
|
||||||
|
Dilemma: Es braucht keinerlei Aktion von Bernd (Heimnetz-Nutzung läuft
|
||||||
|
unverändert weiter, inkl. spontanem Spam-Bann von zu Hause), sperrt aber
|
||||||
|
Fremde aus dem Internet sofort aus. Einzige Einschränkung: Admin-Buttons über
|
||||||
|
die öffentliche URL gehen nicht – die 403-Meldung im Dashboard erklärt das und
|
||||||
|
nennt die Heimnetz-URL. Mit Bernds Entscheidung vom 2026-07-05 (Heimnetz-only,
|
||||||
|
unterwegs WireGuard) ist genau dieser Zustand der gewollte Endzustand. Rückbau
|
||||||
|
(falls je nötig) = die Gate-Zeilen in `server.py` entfernen
|
||||||
|
(Backup: `server.py.bak-20260704-dashgate`).
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# LiveKit-Token server-seitig minten (apiSecret aus dem Client entfernen)
|
||||||
|
|
||||||
|
**Status:** geplant, NICHT umgesetzt (braucht echten Call-Test auf einem Gerät –
|
||||||
|
Calls sind laut CLAUDE.md „heilig"). Dieser Plan ist gegen den echten Code und die
|
||||||
|
echte Pi-Konfiguration geschrieben (2026-07-04), damit ein PC-Termin direkt starten
|
||||||
|
kann. Analog zu `docs/SQLCIPHER_MIGRATION.md`.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`lib/core/livekit_token.dart` mintet die LiveKit-JWTs **im Client**. Dafür muss das
|
||||||
|
LiveKit-`apiSecret` im App-Code liegen (`livekit_token.dart:6`) – d. h. jeder
|
||||||
|
App-Nutzer (auch Uta) trägt das SFU-Secret im Gerät. Wer es hat, kann sich für
|
||||||
|
**jeden** Raum und **jede** Identität ein Beitritts-Token ausstellen. Das ist ein
|
||||||
|
Architekturproblem, kein bloßer Repo-Leak: Selbst nach einem History-Rewrite bliebe
|
||||||
|
das Secret im ausgelieferten Binary.
|
||||||
|
|
||||||
|
Aktueller Fluss (`lib/core/livekit_call_manager.dart:154`):
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final token = LiveKitTokenGenerator.generate(
|
||||||
|
roomName: roomName,
|
||||||
|
identity: identity,
|
||||||
|
displayName: identity,
|
||||||
|
ttlSeconds: 21600, // 6 h
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Erzeugtes JWT (HS256, `livekit_token.dart:14-33`): Grants `video` mit
|
||||||
|
`roomJoin/room/canPublish/canSubscribe/canPublishData`, `metadata` = Anzeigename,
|
||||||
|
`iss` = apiKey (`LKMatrixPi`), `sub` = identity, `exp` = jetzt+ttl.
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Das `apiSecret` liegt **nur noch auf dem Pi**. Der Client holt sich für einen
|
||||||
|
konkreten Raum ein fertiges JWT von einem kleinen Token-Endpoint und übergibt es
|
||||||
|
unverändert an `Room.connect`.
|
||||||
|
|
||||||
|
## Server-Seite (Pi)
|
||||||
|
|
||||||
|
Es gibt bereits einen schlanken Dashboard-Server ohne Fremd-Abhängigkeiten:
|
||||||
|
`/home/steggi/matrix/server.py` (stdlib `http.server`, läuft als
|
||||||
|
`matrix-stats.service` auf `0.0.0.0:8080`, nach außen `dashboard.steggi-matrix.work`).
|
||||||
|
Der Token-Endpoint kann dort als weitere Route `POST /api/livekit-token` andocken –
|
||||||
|
kein neuer Dienst nötig. Das LiveKit-`apiSecret` steht schon auf dem Pi in
|
||||||
|
`/home/steggi/matrix/livekit.yaml` (`keys: LKMatrixPi: <secret>`); der Server liest
|
||||||
|
es von dort, statt es erneut zu hinterlegen (eine Quelle der Wahrheit).
|
||||||
|
|
||||||
|
**Wichtig – Authentifizierung:** Der Endpoint darf NICHT anonym Tokens ausstellen
|
||||||
|
(sonst ist nichts gewonnen). Der Client schickt seinen **Matrix-Access-Token** mit;
|
||||||
|
der Server prüft ihn gegen Continuwuity und leitet daraus die Identität ab:
|
||||||
|
|
||||||
|
1. `POST /api/livekit-token`, Body `{ "room": "<roomName>", "matrix_token": "<access_token>" }`.
|
||||||
|
2. Server ruft `GET http://127.0.0.1:6167/_matrix/client/v3/account/whoami`
|
||||||
|
mit `Authorization: Bearer <matrix_token>` auf → liefert `user_id`
|
||||||
|
(401 → Endpoint gibt 401 zurück, kein Token).
|
||||||
|
3. `identity` = `user_id` (NICHT vom Client wählbar – verhindert Identitäts-Spoofing).
|
||||||
|
4. Optional, aber empfohlen: Raummitgliedschaft prüfen (`/_matrix/client/v3/rooms/
|
||||||
|
{roomId}/joined_members` oder der Matrix-Raum, zu dem der Voice-Channel gehört) –
|
||||||
|
nur ausstellen, wenn der Nutzer wirklich Mitglied ist. Für Phase 1 reicht die
|
||||||
|
whoami-Authentifizierung; Mitgliedschaftsprüfung als Härtung nachziehen.
|
||||||
|
5. Server mintet das LiveKit-JWT **mit denselben Grants wie heute** und gibt
|
||||||
|
`{ "token": "<jwt>", "url": "wss://livekit.steggi-matrix.work" }` zurück.
|
||||||
|
|
||||||
|
**JWT-Minting ohne Fremd-Paket (stdlib genügt):** LiveKit-Tokens sind HS256-JWTs.
|
||||||
|
Mit `hmac`/`hashlib`/`base64`/`json` aus der stdlib:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import base64, hmac, hashlib, json, time
|
||||||
|
|
||||||
|
def _b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=")
|
||||||
|
|
||||||
|
def mint_livekit(api_key, api_secret, identity, room, name, ttl=21600):
|
||||||
|
now = int(time.time())
|
||||||
|
header = {"alg": "HS256", "typ": "JWT"}
|
||||||
|
payload = {
|
||||||
|
"iss": api_key, "sub": identity, "name": name,
|
||||||
|
"nbf": now, "exp": now + ttl, "metadata": name,
|
||||||
|
"video": {"roomJoin": True, "room": room,
|
||||||
|
"canPublish": True, "canSubscribe": True,
|
||||||
|
"canPublishData": True},
|
||||||
|
}
|
||||||
|
segs = [_b64url(json.dumps(header, separators=(",", ":")).encode()),
|
||||||
|
_b64url(json.dumps(payload, separators=(",", ":")).encode())]
|
||||||
|
signing_input = b".".join(segs)
|
||||||
|
sig = hmac.new(api_secret.encode(), signing_input, hashlib.sha256).digest()
|
||||||
|
return (signing_input + b"." + _b64url(sig)).decode()
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifikation lokal auf dem Pi (kein Gerät nötig): geminetes Token gegen den
|
||||||
|
laufenden LiveKit prüfen, z. B. per LiveKit-CLI `lk` oder indem man das JWT auf
|
||||||
|
jwt.io-Art dekodiert und Claims/Signatur vergleicht. Zusätzlich Gegentest mit dem
|
||||||
|
**alten** Client-Pfad: dasselbe Token muss `Room.connect` akzeptieren.
|
||||||
|
|
||||||
|
## Client-Seite
|
||||||
|
|
||||||
|
- `lib/core/livekit_token.dart`: `apiKey`/`apiSecret` **entfernen**. Die Klasse wird
|
||||||
|
entweder gelöscht oder zu einem dünnen HTTP-Client
|
||||||
|
`Future<String> fetchLiveKitToken({required String room, required String matrixToken})`,
|
||||||
|
der `POST https://dashboard.steggi-matrix.work/api/livekit-token` aufruft und das
|
||||||
|
`token`-Feld zurückgibt. `http`-Paket ist schon Abhängigkeit (siehe
|
||||||
|
`update_checker.dart`).
|
||||||
|
- `lib/core/livekit_call_manager.dart:154`: `LiveKitTokenGenerator.generate(...)`
|
||||||
|
→ `await fetchLiveKitToken(room: roomName, matrixToken: matrixClient.accessToken)`.
|
||||||
|
Der Manager hat den Matrix-Client bereits (`_matrixClient`, Zeile 149) – der
|
||||||
|
Access-Token ist also verfügbar. `identity`/`displayName` werden nicht mehr vom
|
||||||
|
Client bestimmt (Server leitet Identität aus whoami ab); den bisherigen
|
||||||
|
`identity`-Parameter entsprechend zurückbauen.
|
||||||
|
- Fehlerpfade: Netzfehler / 401 / 403 sauber als Call-Fehler anzeigen
|
||||||
|
(`error = ...; notifyListeners()`), NICHT still schlucken – sonst „Call verbindet
|
||||||
|
nicht" ohne Hinweis. TTL bleibt 21600 s (6 h), Erneuerung wie bisher beim
|
||||||
|
Neu-Beitreten.
|
||||||
|
|
||||||
|
## Rotation (Pflicht, danach)
|
||||||
|
|
||||||
|
Das alte `apiSecret` `rYUT2PRa…` ist geleakt (Client + Git-History). Nach der
|
||||||
|
Umstellung:
|
||||||
|
|
||||||
|
1. Neues `apiSecret` in `/home/steggi/matrix/livekit.yaml` (`keys:`) setzen.
|
||||||
|
2. LiveKit-Container neu starten
|
||||||
|
(`docker compose -f /home/steggi/matrix/docker-compose.yml restart livekit`).
|
||||||
|
3. `server.py` liest das Secret aus `livekit.yaml` – kein zweiter Ort zu pflegen.
|
||||||
|
4. Alte Clients (Utas installierte APK) minten mit dem alten Secret → deren
|
||||||
|
selbst-gemintete Tokens werden nach der Rotation abgelehnt. Deshalb Rotation
|
||||||
|
**zusammen mit einem neuen Release** ausrollen, das den neuen Fetch-Pfad nutzt.
|
||||||
|
|
||||||
|
## Reihenfolge / Testplan (PC + Gerät)
|
||||||
|
|
||||||
|
1. `server.py`-Route bauen, lokal auf dem Pi verifizieren (whoami-Auth greift,
|
||||||
|
geminetes Token dekodiert korrekt, `Room.connect` akzeptiert es).
|
||||||
|
2. Client umstellen, `flutter analyze` sauber, `flutter test` grün.
|
||||||
|
3. **Auf echtem Gerät** (Calls heilig, kein Headless-Test möglich): Voice-Channel
|
||||||
|
beitreten, Audio/Video/Screenshare, Verlassen/Wieder-Beitreten, zweiter
|
||||||
|
Teilnehmer. Erst wenn das steht: Secret rotieren + Release.
|
||||||
|
4. Danach ROADMAP-M0-Punkt „LiveKit-Token server-seitig minten" abhaken und den
|
||||||
|
Rotations-Teil des Secret-Punkts als erledigt markieren.
|
||||||
|
|
||||||
|
## Warum nicht jetzt (auf dem Pi) gemacht
|
||||||
|
|
||||||
|
Die Server-Route ließe sich hier bauen und headless testen – aber der zugehörige
|
||||||
|
Client-Umbau tauscht den **heiligen Call-Pfad** aus und ist auf diesem Pi ohne
|
||||||
|
GUI/Gerät nicht praktisch prüfbar. Einen halben, nicht end-to-end verifizierbaren
|
||||||
|
Umbau am Call-Pfad zu stapeln verbietet die CLAUDE.md-Leitplanke („keine
|
||||||
|
Verhaltensänderung ohne Not", Calls heilig). Deshalb hier nur der fertige Plan.
|
||||||
@@ -8,6 +8,13 @@ Punkt hier eintragen (✅/❌ + Datum), Befunde als eigene PROGRESS-Einträge.
|
|||||||
|
|
||||||
Schnellster Weg: `flutter run -d windows`.
|
Schnellster Weg: `flutter run -d windows`.
|
||||||
|
|
||||||
|
## 0. Vorab (1 Minute)
|
||||||
|
|
||||||
|
- [ ] `flutter test` – die Regressionstests unter `test/` (Soft-Logout-Guard,
|
||||||
|
Power-Levels-Schutz, AuthLog, Storage-Fassade `settings_prefs` –
|
||||||
|
Stand 2026-07-03: 24 Tests) müssen grün sein, bevor irgendetwas
|
||||||
|
anderes geprüft wird.
|
||||||
|
|
||||||
## 1. Härtetest Login/Krypto (M1 – PFLICHT, zuerst!)
|
## 1. Härtetest Login/Krypto (M1 – PFLICHT, zuerst!)
|
||||||
|
|
||||||
Deckt die Fixes 63e4919, 9dc83f7, 891f348 ab (Uta-Random-Logout + Guard).
|
Deckt die Fixes 63e4919, 9dc83f7, 891f348 ab (Uta-Random-Logout + Guard).
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ Continuwuity hat einen speziellen Admin-Raum. Als Server-Admin kannst du dort Be
|
|||||||
# Als curl (Admin-Token erforderlich):
|
# Als curl (Admin-Token erforderlich):
|
||||||
# Admin-Room-ID herausfinden:
|
# Admin-Room-ID herausfinden:
|
||||||
curl -s "https://steggi-matrix.work/_matrix/client/v3/directory/room/%23admins%3Asteggi-matrix.work" \
|
curl -s "https://steggi-matrix.work/_matrix/client/v3/directory/room/%23admins%3Asteggi-matrix.work" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||||
# → {"room_id": "!adminRoomId:steggi-matrix.work", ...}
|
# → {"room_id": "!adminRoomId:steggi-matrix.work", ...}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -84,22 +84,22 @@ curl -s "https://steggi-matrix.work/_matrix/client/v3/directory/room/%23admins%3
|
|||||||
|
|
||||||
# User zu Server-Admin machen
|
# User zu Server-Admin machen
|
||||||
curl -X POST "https://steggi-matrix.work/_conduwuit/admin/v1/make_user_admin" \
|
curl -X POST "https://steggi-matrix.work/_conduwuit/admin/v1/make_user_admin" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"user_id": "@todesneutron:steggi-matrix.work"}'
|
-d '{"user_id": "@todesneutron:steggi-matrix.work"}'
|
||||||
|
|
||||||
# Server-Status abfragen
|
# Server-Status abfragen
|
||||||
curl "https://steggi-matrix.work/_conduwuit/server/version" \
|
curl "https://steggi-matrix.work/_conduwuit/server/version" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||||
|
|
||||||
# Alternativ — Standard Matrix-Whoami (prüft ob Token gültig):
|
# Alternativ — Standard Matrix-Whoami (prüft ob Token gültig):
|
||||||
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||||
# → {"user_id": "@todesneutron:steggi-matrix.work", "is_guest": false}
|
# → {"user_id": "@todesneutron:steggi-matrix.work", "is_guest": false}
|
||||||
|
|
||||||
# Prüfen ob User Server-Admin ist (Conduwuit-spezifisch):
|
# Prüfen ob User Server-Admin ist (Conduwuit-spezifisch):
|
||||||
curl "https://steggi-matrix.work/_conduwuit/admin/v1/users/@todesneutron:steggi-matrix.work" \
|
curl "https://steggi-matrix.work/_conduwuit/admin/v1/users/@todesneutron:steggi-matrix.work" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -112,7 +112,7 @@ Als Server-Admin kannst du via Matrix Client API Power Levels in jedem Raum setz
|
|||||||
```bash
|
```bash
|
||||||
# Schritt 1: Aktuelles Power-Level-Event lesen
|
# Schritt 1: Aktuelles Power-Level-Event lesen
|
||||||
ROOM_ID="!raumId:steggi-matrix.work"
|
ROOM_ID="!raumId:steggi-matrix.work"
|
||||||
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
TOKEN="<ADMIN_TOKEN>"
|
||||||
SERVER="https://steggi-matrix.work"
|
SERVER="https://steggi-matrix.work"
|
||||||
|
|
||||||
curl "${SERVER}/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \
|
curl "${SERVER}/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \
|
||||||
@@ -172,7 +172,7 @@ await client.setRoomStateWithKey(
|
|||||||
```bash
|
```bash
|
||||||
# Testen ob restricted join rules funktionieren:
|
# Testen ob restricted join rules funktionieren:
|
||||||
ROOM_ID="!testRaum:steggi-matrix.work"
|
ROOM_ID="!testRaum:steggi-matrix.work"
|
||||||
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
TOKEN="<ADMIN_TOKEN>"
|
||||||
SPACE_ID="!spaceId:steggi-matrix.work"
|
SPACE_ID="!spaceId:steggi-matrix.work"
|
||||||
|
|
||||||
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.join_rules/" \
|
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.join_rules/" \
|
||||||
@@ -230,7 +230,7 @@ try {
|
|||||||
```bash
|
```bash
|
||||||
# 1. Prüfen: Welchen Power Level hat mein User in diesem Raum?
|
# 1. Prüfen: Welchen Power Level hat mein User in diesem Raum?
|
||||||
ROOM_ID="!raumId:steggi-matrix.work"
|
ROOM_ID="!raumId:steggi-matrix.work"
|
||||||
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
TOKEN="<ADMIN_TOKEN>"
|
||||||
|
|
||||||
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \
|
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \
|
||||||
-H "Authorization: Bearer ${TOKEN}"
|
-H "Authorization: Bearer ${TOKEN}"
|
||||||
@@ -269,7 +269,7 @@ curl "https://steggi-matrix.work/_matrix/client/v3/capabilities" \
|
|||||||
```bash
|
```bash
|
||||||
# Prüfen ob User Server-Admin ist:
|
# Prüfen ob User Server-Admin ist:
|
||||||
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||||
|
|
||||||
# Auf Pi (SSH): User-Datenbank direkt abfragen (Conduwuit/Continuwuity nutzt RocksDB)
|
# Auf Pi (SSH): User-Datenbank direkt abfragen (Conduwuit/Continuwuity nutzt RocksDB)
|
||||||
# Alternativ: Über Admin-Room-Befehl
|
# Alternativ: Über Admin-Room-Befehl
|
||||||
@@ -278,7 +278,7 @@ curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
|||||||
|
|
||||||
# Über HTTP (Conduwuit Admin-Endpoint):
|
# Über HTTP (Conduwuit Admin-Endpoint):
|
||||||
curl -X POST "https://steggi-matrix.work/_conduwuit/admin/v1/make_user_admin" \
|
curl -X POST "https://steggi-matrix.work/_conduwuit/admin/v1/make_user_admin" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"user_id": "@todesneutron:steggi-matrix.work"}'
|
-d '{"user_id": "@todesneutron:steggi-matrix.work"}'
|
||||||
```
|
```
|
||||||
@@ -292,19 +292,19 @@ Da kein Synapse-Admin-API existiert, nutze die Standard-Matrix-Client-API mit Ad
|
|||||||
```bash
|
```bash
|
||||||
# Raum beitreten (als Admin, auch wenn invite-only):
|
# Raum beitreten (als Admin, auch wenn invite-only):
|
||||||
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/join/!roomId%3Asteggi-matrix.work" \
|
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/join/!roomId%3Asteggi-matrix.work" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{}'
|
-d '{}'
|
||||||
|
|
||||||
# User in Raum einladen:
|
# User in Raum einladen:
|
||||||
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/invite" \
|
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/invite" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"user_id": "@user:steggi-matrix.work"}'
|
-d '{"user_id": "@user:steggi-matrix.work"}'
|
||||||
|
|
||||||
# Space-Child-Relation setzen (Channel einem Space hinzufügen):
|
# Space-Child-Relation setzen (Channel einem Space hinzufügen):
|
||||||
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!spaceId%3Asteggi-matrix.work/state/m.space.child/!channelId%3Asteggi-matrix.work/" \
|
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!spaceId%3Asteggi-matrix.work/state/m.space.child/!channelId%3Asteggi-matrix.work/" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"via": ["steggi-matrix.work"],
|
"via": ["steggi-matrix.work"],
|
||||||
@@ -313,7 +313,7 @@ curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!spaceId%3Astegg
|
|||||||
|
|
||||||
# Space-Parent-Relation setzen (im Channel-Raum):
|
# Space-Parent-Relation setzen (im Channel-Raum):
|
||||||
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!channelId%3Asteggi-matrix.work/state/m.space.parent/!spaceId%3Asteggi-matrix.work/" \
|
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!channelId%3Asteggi-matrix.work/state/m.space.parent/!spaceId%3Asteggi-matrix.work/" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"via": ["steggi-matrix.work"],
|
"via": ["steggi-matrix.work"],
|
||||||
@@ -322,11 +322,11 @@ curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!channelId%3Aste
|
|||||||
|
|
||||||
# Alle Mitglieder eines Raums auflisten:
|
# Alle Mitglieder eines Raums auflisten:
|
||||||
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/members" \
|
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/members" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||||
|
|
||||||
# Raum verlassen (als User, nicht löschen):
|
# Raum verlassen (als User, nicht löschen):
|
||||||
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/leave" \
|
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/leave" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{}'
|
-d '{}'
|
||||||
```
|
```
|
||||||
@@ -419,15 +419,15 @@ Wenn du M_FORBIDDEN bekommst:
|
|||||||
```bash
|
```bash
|
||||||
# 1. Token gültig?
|
# 1. Token gültig?
|
||||||
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||||
|
|
||||||
# 2. PL im betroffenen Raum prüfen:
|
# 2. PL im betroffenen Raum prüfen:
|
||||||
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!ROOM_ID/state/m.room.power_levels/" \
|
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!ROOM_ID/state/m.room.power_levels/" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||||
|
|
||||||
# 3. Mitglied des Raums?
|
# 3. Mitglied des Raums?
|
||||||
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!ROOM_ID/state/m.room.member/%40todesneutron%3Asteggi-matrix.work/" \
|
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!ROOM_ID/state/m.room.member/%40todesneutron%3Asteggi-matrix.work/" \
|
||||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||||
|
|
||||||
# 4. Was genau schlägt fehl? (Antwort-Body lesen!)
|
# 4. Was genau schlägt fehl? (Antwort-Body lesen!)
|
||||||
# M_FORBIDDEN + "Power level too low" → PL erhöhen
|
# M_FORBIDDEN + "Power level too low" → PL erhöhen
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
|
|
||||||
class BoolPref extends StateNotifier<bool> {
|
class BoolPref extends StateNotifier<bool> {
|
||||||
final String _key;
|
final String _key;
|
||||||
|
// Guards against the load race: if set()/toggle() ran before _load's read
|
||||||
|
// lands, the stale persisted value must not overwrite the user's choice.
|
||||||
|
bool _userSet = false;
|
||||||
|
|
||||||
BoolPref(this._key, bool defaultValue) : super(defaultValue) {
|
BoolPref(this._key, bool defaultValue) : super(defaultValue) {
|
||||||
_load(defaultValue);
|
_load(defaultValue);
|
||||||
@@ -14,10 +17,11 @@ class BoolPref extends StateNotifier<bool> {
|
|||||||
|
|
||||||
Future<void> _load(bool def) async {
|
Future<void> _load(bool def) async {
|
||||||
final p = await SharedPreferences.getInstance();
|
final p = await SharedPreferences.getInstance();
|
||||||
if (mounted) state = p.getBool(_key) ?? def;
|
if (mounted && !_userSet) state = p.getBool(_key) ?? def;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> set(bool v) async {
|
Future<void> set(bool v) async {
|
||||||
|
_userSet = true;
|
||||||
state = v;
|
state = v;
|
||||||
final p = await SharedPreferences.getInstance();
|
final p = await SharedPreferences.getInstance();
|
||||||
await p.setBool(_key, v);
|
await p.setBool(_key, v);
|
||||||
@@ -30,9 +34,10 @@ class BoolPref extends StateNotifier<bool> {
|
|||||||
|
|
||||||
class StringSetPref extends StateNotifier<Set<String>> {
|
class StringSetPref extends StateNotifier<Set<String>> {
|
||||||
final String _key;
|
final String _key;
|
||||||
|
late final Future<void> _loaded;
|
||||||
|
|
||||||
StringSetPref(this._key) : super({}) {
|
StringSetPref(this._key) : super({}) {
|
||||||
_load();
|
_loaded = _load();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _load() async {
|
Future<void> _load() async {
|
||||||
@@ -42,14 +47,18 @@ class StringSetPref extends StateNotifier<Set<String>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> add(String v) async {
|
Future<void> add(String v) async {
|
||||||
if (state.contains(v)) return;
|
// Read-modify-write: ohne abgeschlossenes Laden würde hier der halbleere
|
||||||
|
// Start-State zurückgeschrieben und persistierte Einträge gingen verloren.
|
||||||
|
await _loaded;
|
||||||
|
if (!mounted || state.contains(v)) return;
|
||||||
state = {...state, v};
|
state = {...state, v};
|
||||||
final p = await SharedPreferences.getInstance();
|
final p = await SharedPreferences.getInstance();
|
||||||
await p.setStringList(_key, state.toList());
|
await p.setStringList(_key, state.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> remove(String v) async {
|
Future<void> remove(String v) async {
|
||||||
if (!state.contains(v)) return;
|
await _loaded;
|
||||||
|
if (!mounted || !state.contains(v)) return;
|
||||||
state = {...state}..remove(v);
|
state = {...state}..remove(v);
|
||||||
final p = await SharedPreferences.getInstance();
|
final p = await SharedPreferences.getInstance();
|
||||||
await p.setStringList(_key, state.toList());
|
await p.setStringList(_key, state.toList());
|
||||||
|
|||||||
@@ -428,7 +428,11 @@ class _EncryptionSectionState extends ConsumerState<_EncryptionSection> {
|
|||||||
final notifier = ref.read(e2eeDiagnosticsProvider.notifier);
|
final notifier = ref.read(e2eeDiagnosticsProvider.notifier);
|
||||||
final msg = await notifier.upload(
|
final msg = await notifier.upload(
|
||||||
serverUrl: 'https://dashboard.steggi-matrix.work',
|
serverUrl: 'https://dashboard.steggi-matrix.work',
|
||||||
token: 'J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI',
|
// Eng begrenzter Diagnose-Token (haengt nur an den Diagnose-Log an,
|
||||||
|
// KEINE Admin-Rechte). Bewusst NICHT der Matrix-Server-Admin-Token –
|
||||||
|
// der darf niemals im Client landen. Passendes Gegenstueck in
|
||||||
|
// /home/steggi/matrix/server.py (DIAG_TOKEN) auf dem Pi.
|
||||||
|
token: 'pyr-diag-6AyELgODRv-4rF4dcau3kSa5YbgZqUcn',
|
||||||
);
|
);
|
||||||
if (mounted) setState(() { _diagUploading = false; _diagMsgIsError = false; _diagMsg = msg; });
|
if (mounted) setState(() { _diagUploading = false; _diagMsgIsError = false; _diagMsg = msg; });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+7
-7
@@ -801,10 +801,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -918,7 +918,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
@@ -1294,18 +1294,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.11"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.16"
|
version: "0.6.17"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ dev_dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
# Nur für Tests: offizieller Weg, path_provider ohne echte Plattform zu
|
||||||
|
# mocken (PathProviderPlatform.instance überschreiben). War schon transitiv
|
||||||
|
# im Lockfile, kein neues externes Paket.
|
||||||
|
path_provider_platform_interface: ^2.1.2
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
webcrypto:
|
webcrypto:
|
||||||
|
|||||||
+11
-1
@@ -11,7 +11,17 @@ Set-Location (Split-Path $PSScriptRoot -Parent)
|
|||||||
# ── Config ────────────────────────────────────────────────────────────────────
|
# ── Config ────────────────────────────────────────────────────────────────────
|
||||||
$GiteaBase = "http://192.168.178.71:3000"
|
$GiteaBase = "http://192.168.178.71:3000"
|
||||||
$GiteaRepo = "steggi/pyramid"
|
$GiteaRepo = "steggi/pyramid"
|
||||||
$GiteaToken = "bb522f9646c6856c9ab58bef0151ac36a9ce1d57"
|
# Token NIE hier eintragen (CLAUDE.md: keine Secrets ins Repo). Er kommt aus der
|
||||||
|
# Umgebungsvariable PYRAMID_GITEA_TOKEN – einmalig pro PC setzen mit:
|
||||||
|
# [Environment]::SetEnvironmentVariable("PYRAMID_GITEA_TOKEN", "<token>", "User")
|
||||||
|
# (neues Terminal öffnen, damit die Variable sichtbar ist)
|
||||||
|
$GiteaToken = $env:PYRAMID_GITEA_TOKEN
|
||||||
|
if ([string]::IsNullOrWhiteSpace($GiteaToken)) {
|
||||||
|
Write-Error ("PYRAMID_GITEA_TOKEN ist nicht gesetzt. Einmalig setzen mit:`n" +
|
||||||
|
' [Environment]::SetEnvironmentVariable("PYRAMID_GITEA_TOKEN", "<token>", "User")' +
|
||||||
|
"`nDen Token in Gitea unter Einstellungen > Anwendungen erzeugen (Scope: repository).")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
$Headers = @{ Authorization = "token $GiteaToken"; "Content-Type" = "application/json" }
|
$Headers = @{ Authorization = "token $GiteaToken"; "Content-Type" = "application/json" }
|
||||||
|
|
||||||
# ── Read and Update version from pubspec.yaml ─────────────────────────────────
|
# ── Read and Update version from pubspec.yaml ─────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||||
|
import 'package:pyramid/core/auth_log.dart';
|
||||||
|
|
||||||
|
/// Plattform kaputt/nicht verfügbar (z. B. sehr früher App-Start, exotischer
|
||||||
|
/// Fehler): genau der Fall, in dem AuthLog.write NIEMALS werfen darf – der
|
||||||
|
/// Soft-Logout-Guard ruft es in seinem catch-Pfad auf, und ein Throw dort
|
||||||
|
/// würde im SDK zu logout()+clear() eskalieren (der Uta-Bug).
|
||||||
|
class _BrokenPathProvider extends PathProviderPlatform {
|
||||||
|
@override
|
||||||
|
Future<String?> getApplicationSupportPath() async =>
|
||||||
|
throw StateError('Plattform nicht verfügbar');
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TempPathProvider extends PathProviderPlatform {
|
||||||
|
_TempPathProvider(this.path);
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getApplicationSupportPath() async => path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// WICHTIG: Reihenfolge der Tests ist Absicht. AuthLog cached sein File-Handle
|
||||||
|
// statisch; der Kaputt-Plattform-Test muss laufen, BEVOR ein Test mit
|
||||||
|
// funktionierender Plattform das Handle in den Cache legt.
|
||||||
|
late Directory tempDir;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
tempDir = Directory.systemTemp.createTempSync('pyramid_auth_log_test');
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() {
|
||||||
|
tempDir.deleteSync(recursive: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'write/read werfen NIE, auch wenn die Plattform fehlt '
|
||||||
|
'(CLAUDE.md: Fehlerpfade dürfen nie in einem stillen Logout enden)',
|
||||||
|
() async {
|
||||||
|
PathProviderPlatform.instance = _BrokenPathProvider();
|
||||||
|
// Darf normal zurückkehren, obwohl getApplicationSupportPath wirft.
|
||||||
|
await AuthLog.write('darf nicht werfen');
|
||||||
|
expect(await AuthLog.read(), startsWith('Log konnte nicht gelesen werden'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('read ohne bisherige Einträge liefert Platzhalter statt Fehler',
|
||||||
|
() async {
|
||||||
|
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
|
||||||
|
expect(await AuthLog.read(), '(noch keine Einträge)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('write hängt Zeile mit ISO-Zeitstempel an, read liefert sie zurück',
|
||||||
|
() async {
|
||||||
|
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
|
||||||
|
await AuthLog.write('Hallo Log');
|
||||||
|
final content = await AuthLog.read();
|
||||||
|
expect(content, contains('Hallo Log'));
|
||||||
|
expect(
|
||||||
|
RegExp(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}').hasMatch(content),
|
||||||
|
isTrue,
|
||||||
|
reason: 'jede Zeile beginnt mit einem ISO-8601-Zeitstempel',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Log wird auf 500 Zeilen gekappt (älteste fliegen raus), '
|
||||||
|
'damit es nie unbegrenzt wächst', () async {
|
||||||
|
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
|
||||||
|
for (var i = 0; i < 520; i++) {
|
||||||
|
await AuthLog.write('Zeile $i');
|
||||||
|
}
|
||||||
|
final lines = (await AuthLog.read()).trimRight().split('\n');
|
||||||
|
expect(lines.length, 500);
|
||||||
|
expect(lines.last, contains('Zeile 519'),
|
||||||
|
reason: 'neueste Einträge müssen überleben');
|
||||||
|
expect((await AuthLog.read()).contains('Hallo Log'), isFalse,
|
||||||
|
reason: 'ältester Eintrag (aus dem Test davor) muss weggekappt sein');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:pyramid/features/spaces/space_admin_dialog.dart';
|
||||||
|
|
||||||
|
/// Fake-Client für `updatePowerLevelsSafely`: liefert einen vorgegebenen
|
||||||
|
/// Server-Stand des power_levels-Events und zeichnet auf, was (wenn
|
||||||
|
/// überhaupt) zurückgeschrieben wird. Alles andere → NoSuchMethodError.
|
||||||
|
class _FakeClient implements Client {
|
||||||
|
_FakeClient({required this.serverState, this.getError});
|
||||||
|
|
||||||
|
Map<String, Object?> serverState;
|
||||||
|
MatrixException? getError;
|
||||||
|
Map<String, Object?>? written;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get userID => '@me:pyramid.example';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, Object?>> getRoomStateWithKey(
|
||||||
|
String roomId,
|
||||||
|
String eventType,
|
||||||
|
String stateKey, {
|
||||||
|
Format? format,
|
||||||
|
}) async {
|
||||||
|
final err = getError;
|
||||||
|
if (err != null) throw err;
|
||||||
|
return serverState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> setRoomStateWithKey(
|
||||||
|
String roomId,
|
||||||
|
String eventType,
|
||||||
|
String stateKey,
|
||||||
|
Map<String, Object?> body,
|
||||||
|
) async {
|
||||||
|
written = body;
|
||||||
|
return r'$neuesEvent';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeRoom implements Room {
|
||||||
|
_FakeRoom(this.client);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Client client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => '!raum:pyramid.example';
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('Server-Stand bleibt erhalten: fremde Einträge werden nie überschrieben '
|
||||||
|
'(genau der Bug, der schon einmal Admins ausgesperrt hat)', () async {
|
||||||
|
final client = _FakeClient(serverState: {
|
||||||
|
'users': {'@me:pyramid.example': 100, '@uta:pyramid.example': 50},
|
||||||
|
'users_default': 0,
|
||||||
|
'state_default': 50,
|
||||||
|
});
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
await updatePowerLevelsSafely(room, (content) {
|
||||||
|
(content['users'] as Map<String, dynamic>)['@neu:pyramid.example'] = 50;
|
||||||
|
});
|
||||||
|
|
||||||
|
final users = client.written!['users'] as Map;
|
||||||
|
expect(users['@uta:pyramid.example'], 50,
|
||||||
|
reason: 'bestehender Server-Eintrag muss überleben');
|
||||||
|
expect(users['@me:pyramid.example'], 100);
|
||||||
|
expect(users['@neu:pyramid.example'], 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Selbst-Degradierung unter das nötige Level wird abgebrochen, '
|
||||||
|
'nichts wird geschrieben', () async {
|
||||||
|
final client = _FakeClient(serverState: {
|
||||||
|
'users': {'@me:pyramid.example': 100},
|
||||||
|
'state_default': 50,
|
||||||
|
});
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
updatePowerLevelsSafely(room, (content) {
|
||||||
|
(content['users'] as Map<String, dynamic>)['@me:pyramid.example'] = 0;
|
||||||
|
}),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
expect(client.written, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eigener Eintrag entfernt → Fallback auf users_default greift und '
|
||||||
|
'schützt ebenfalls', () async {
|
||||||
|
final client = _FakeClient(serverState: {
|
||||||
|
'users': {'@me:pyramid.example': 100},
|
||||||
|
'users_default': 0,
|
||||||
|
});
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
updatePowerLevelsSafely(room, (content) {
|
||||||
|
(content['users'] as Map<String, dynamic>)
|
||||||
|
.remove('@me:pyramid.example');
|
||||||
|
}),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
expect(client.written, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('explizites events[m.room.power_levels]-Erfordernis wird respektiert',
|
||||||
|
() async {
|
||||||
|
final client = _FakeClient(serverState: {
|
||||||
|
'users': {'@me:pyramid.example': 60},
|
||||||
|
'events': {'m.room.power_levels': 75},
|
||||||
|
'state_default': 50,
|
||||||
|
});
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
// 60 < 75 → muss abbrechen, obwohl state_default (50) erfüllt wäre.
|
||||||
|
await expectLater(
|
||||||
|
updatePowerLevelsSafely(room, (_) {}),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
expect(client.written, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fehlendes power_levels-Event (M_NOT_FOUND): Start mit leerem Event, '
|
||||||
|
'Schreiben nur wenn man sich selbst hoch genug setzt', () async {
|
||||||
|
final client = _FakeClient(
|
||||||
|
serverState: {},
|
||||||
|
getError: MatrixException.fromJson(
|
||||||
|
{'errcode': 'M_NOT_FOUND', 'error': 'Event not found.'}),
|
||||||
|
);
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
// Ohne eigenen Eintrag: users_default 0 < state_default-Fallback 50 → Abbruch.
|
||||||
|
await expectLater(
|
||||||
|
updatePowerLevelsSafely(room, (_) {}),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
expect(client.written, isNull);
|
||||||
|
|
||||||
|
// Setzt man sich selbst auf 100, darf geschrieben werden.
|
||||||
|
await updatePowerLevelsSafely(room, (content) {
|
||||||
|
(content['users'] as Map<String, dynamic>)['@me:pyramid.example'] = 100;
|
||||||
|
});
|
||||||
|
final users = client.written!['users'] as Map;
|
||||||
|
expect(users['@me:pyramid.example'], 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('andere Serverfehler (z. B. M_FORBIDDEN) werden durchgereicht, '
|
||||||
|
'nichts wird geschrieben', () async {
|
||||||
|
final client = _FakeClient(
|
||||||
|
serverState: {},
|
||||||
|
getError: MatrixException.fromJson(
|
||||||
|
{'errcode': 'M_FORBIDDEN', 'error': 'Nope.'}),
|
||||||
|
);
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
updatePowerLevelsSafely(room, (_) {}),
|
||||||
|
throwsA(isA<MatrixException>()),
|
||||||
|
);
|
||||||
|
expect(client.written, isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pyramid/core/settings_prefs.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
/// Tests für die Storage-Fassade, die laut M2-Modulschnitt zur Pflicht-
|
||||||
|
/// Schnittstelle für alle Einstellungs-Zugriffe ausgebaut werden soll –
|
||||||
|
/// dieses Vertragsnetz muss bei dem Umbau stehen bleiben.
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
// Frischer In-Memory-Store pro Test (setzt auch die gecachte
|
||||||
|
// SharedPreferences-Instanz zurück).
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('BoolPref', () {
|
||||||
|
test('lädt persistierten Wert und überstimmt damit den Default', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({'notif_sound': false});
|
||||||
|
final pref = BoolPref('notif_sound', true);
|
||||||
|
expect(pref.state, true, reason: 'Default gilt, bis geladen ist');
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(pref.state, false, reason: 'persistierter Wert muss gewinnen');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'set() sofort nach Erstellung gewinnt gegen den später ladenden '
|
||||||
|
'persistierten Wert (Lade-Race)', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({'notif_sound': false});
|
||||||
|
final pref = BoolPref('notif_sound', false);
|
||||||
|
final done = pref.set(true); // bevor _load fertig ist
|
||||||
|
await pumpEventQueue();
|
||||||
|
await done;
|
||||||
|
expect(pref.state, true,
|
||||||
|
reason: 'die Nutzeraktion darf nicht vom Lade-Ergebnis '
|
||||||
|
'zurücküberschrieben werden');
|
||||||
|
final p = await SharedPreferences.getInstance();
|
||||||
|
expect(p.getBool('notif_sound'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set/toggle ändern Zustand und persistieren', () async {
|
||||||
|
final pref = BoolPref('notif_desktop', true);
|
||||||
|
await pumpEventQueue();
|
||||||
|
pref.toggle();
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(pref.state, false);
|
||||||
|
final p = await SharedPreferences.getInstance();
|
||||||
|
expect(p.getBool('notif_desktop'), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('StringSetPref', () {
|
||||||
|
test('lädt persistierte Liste als Set, add/remove persistieren', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'muted_rooms': ['!a:hs', '!b:hs'],
|
||||||
|
});
|
||||||
|
final pref = StringSetPref('muted_rooms');
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(pref.state, {'!a:hs', '!b:hs'});
|
||||||
|
|
||||||
|
await pref.add('!c:hs');
|
||||||
|
await pref.remove('!a:hs');
|
||||||
|
final p = await SharedPreferences.getInstance();
|
||||||
|
expect(p.getStringList('muted_rooms')!.toSet(), {'!b:hs', '!c:hs'});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'add() sofort nach Erstellung verliert keine bereits persistierten '
|
||||||
|
'Einträge (Lade-Race)', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'muted_rooms': ['!a:hs'],
|
||||||
|
});
|
||||||
|
final pref = StringSetPref('muted_rooms');
|
||||||
|
final done = pref.add('!b:hs'); // bevor _load fertig ist
|
||||||
|
await pumpEventQueue();
|
||||||
|
await done;
|
||||||
|
expect(pref.state, {'!a:hs', '!b:hs'});
|
||||||
|
final p = await SharedPreferences.getInstance();
|
||||||
|
expect(p.getStringList('muted_rooms')!.toSet(), {'!a:hs', '!b:hs'},
|
||||||
|
reason: 'Read-Modify-Write auf ungeladenem State darf keine '
|
||||||
|
'persistierten Einträge verlieren');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('doppeltes add und remove von Fehlendem sind No-Ops', () async {
|
||||||
|
final pref = StringSetPref('muted_rooms');
|
||||||
|
await pumpEventQueue();
|
||||||
|
await pref.add('!a:hs');
|
||||||
|
await pref.add('!a:hs');
|
||||||
|
await pref.remove('!gibtsnicht:hs');
|
||||||
|
expect(pref.state, {'!a:hs'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Theme-Prefs', () {
|
||||||
|
test('leerer Store liefert die dokumentierten Defaults', () async {
|
||||||
|
final t = await loadThemePrefs();
|
||||||
|
expect(t.isDark, true);
|
||||||
|
expect(t.accentIdx, 0);
|
||||||
|
expect(t.radius, 12.0);
|
||||||
|
expect(t.motion, 1.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Roundtrip: gespeicherte Werte kommen zurück, Teil-Save lässt '
|
||||||
|
'den Rest unangetastet', () async {
|
||||||
|
await saveThemePrefs(isDark: false, accentIdx: 3, radius: 8, motion: 0.5);
|
||||||
|
var t = await loadThemePrefs();
|
||||||
|
expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 3, 8.0, 0.5));
|
||||||
|
|
||||||
|
// Nur ein Feld ändern – die anderen dürfen nicht zurückfallen.
|
||||||
|
await saveThemePrefs(accentIdx: 1);
|
||||||
|
t = await loadThemePrefs();
|
||||||
|
expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 1, 8.0, 0.5));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Server-Banner', () {
|
||||||
|
test('Roundtrip + Entfernen per null/leerem mxcUri', () async {
|
||||||
|
await saveServerBanner('https://hs1', 'mxc://hs1/abc');
|
||||||
|
await saveServerBanner('https://hs2', 'mxc://hs2/def');
|
||||||
|
expect(await loadAllServerBanners(),
|
||||||
|
{'https://hs1': 'mxc://hs1/abc', 'https://hs2': 'mxc://hs2/def'});
|
||||||
|
|
||||||
|
await saveServerBanner('https://hs1', null);
|
||||||
|
await saveServerBanner('https://hs2', '');
|
||||||
|
expect(await loadAllServerBanners(), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('korruptes JSON im Store crasht nicht, sondern liefert leere Map',
|
||||||
|
() async {
|
||||||
|
SharedPreferences.setMockInitialValues(
|
||||||
|
{'server_banners': '{kein json'});
|
||||||
|
expect(await loadAllServerBanners(), isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:pyramid/core/soft_logout_guard.dart';
|
||||||
|
|
||||||
|
/// Fake-Client: nur `refreshAccessToken` ist implementiert. Jeder andere
|
||||||
|
/// SDK-Aufruf (insbesondere `logout()`/`clear()`!) schlägt laut mit
|
||||||
|
/// NoSuchMethodError fehl – so würde der Test es sofort merken, wenn der
|
||||||
|
/// Guard je etwas anderes am Client aufriefe als den Refresh.
|
||||||
|
class _FakeClient implements Client {
|
||||||
|
_FakeClient(this._onRefresh);
|
||||||
|
|
||||||
|
final Future<void> Function(int attempt) _onRefresh;
|
||||||
|
int attempts = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> refreshAccessToken({Duration? customRefreshTokenLifetime}) {
|
||||||
|
attempts++;
|
||||||
|
return _onRefresh(attempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('erfolgreicher Refresh beim ersten Versuch: genau 1 Aufruf', () async {
|
||||||
|
final client = _FakeClient((_) async {});
|
||||||
|
await guardedSoftLogoutRefresh(client);
|
||||||
|
expect(client.attempts, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'transienter Fehler (z. B. Netz weg / Token-Rotations-Race mit dem '
|
||||||
|
'Push-Isolate): Retry heilt, kein Fehler nach außen',
|
||||||
|
() async {
|
||||||
|
final client = _FakeClient((attempt) async {
|
||||||
|
if (attempt == 1) throw Exception('Netzwerkfehler');
|
||||||
|
});
|
||||||
|
await guardedSoftLogoutRefresh(client);
|
||||||
|
expect(client.attempts, 2);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'dauerhafter Fehler: genau 3 Versuche, und der Guard wirft NIE '
|
||||||
|
'(CLAUDE.md: Fehlerpfade dürfen nie in einem stillen Logout enden – '
|
||||||
|
'ein Throw hier würde im SDK zu logout()+clear() eskalieren)',
|
||||||
|
() async {
|
||||||
|
final client = _FakeClient((_) async {
|
||||||
|
throw Exception('Server dauerhaft nicht erreichbar');
|
||||||
|
});
|
||||||
|
// Darf trotz 3 Fehlversuchen normal (ohne Exception) zurückkehren.
|
||||||
|
await guardedSoftLogoutRefresh(client);
|
||||||
|
expect(client.attempts, 3);
|
||||||
|
},
|
||||||
|
// Der Guard wartet real 2s+4s zwischen den Versuchen.
|
||||||
|
timeout: const Timeout(Duration(seconds: 30)),
|
||||||
|
);
|
||||||
|
|
||||||
|
test('auch ein Error (nicht nur Exception) im Refresh entkommt nie', () async {
|
||||||
|
final client = _FakeClient((_) async {
|
||||||
|
throw StateError('unerwarteter Zustand');
|
||||||
|
});
|
||||||
|
await guardedSoftLogoutRefresh(client);
|
||||||
|
expect(client.attempts, 3);
|
||||||
|
}, timeout: const Timeout(Duration(seconds: 30)));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user