docs: Auth-Abschnitt, korrigierte Test-Anleitung, Phase 9
deployment.md bekommt einen eigenen Abschnitt zur Authentifizierung (Schlüsselweg, geschützte Endpoints, Fehlercodes, der Schalter für VPN-Instanzen) und eine CORS-Beschreibung, die die Allowlist statt des alten Wildcards erklärt — inklusive der Tauri-Origins, die sonst am Preflight scheitern. Im README steht jetzt der Hinweis, der diese Runde am meisten gekostet hat: ohne DATABASE_URL_APPVIEW in der Umgebung überspringen sich die DB-Tests selbst und `cargo test --workspace` meldet grün, ohne sie ausgeführt zu haben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
This commit is contained in:
co-authored by
Claude Opus 5
parent
9ee717bbc7
commit
f7b78fd5db
@@ -68,6 +68,7 @@ cargo run -p appview
|
|||||||
| 6 Tauri-UI-Logik an Backend koppeln | ✅ done — LoginScreen, NavRail, PostCard, ComposeBox, Profile/Compose/Search/Settings-Views |
|
| 6 Tauri-UI-Logik an Backend koppeln | ✅ done — LoginScreen, NavRail, PostCard, ComposeBox, Profile/Compose/Search/Settings-Views |
|
||||||
| 7 Polish (Tray, Notifications, Auto-Update) | ✅ done — Tray-Icon custom (`tauri::include_image!`), Notification-Click navigiert via `app://notification`-Event + `openThread`-Helper zu Thread-Detail, Auto-Update in Dev inert (Production-Weg: [`docs/tauri-release.md`](docs/tauri-release.md)) |
|
| 7 Polish (Tray, Notifications, Auto-Update) | ✅ done — Tray-Icon custom (`tauri::include_image!`), Notification-Click navigiert via `app://notification`-Event + `openThread`-Helper zu Thread-Detail, Auto-Update in Dev inert (Production-Weg: [`docs/tauri-release.md`](docs/tauri-release.md)) |
|
||||||
| 8 Social-Graph + Benachrichtigungen | ✅ done — `notifications`-Tabelle, Schreibpfad im Jetstream-Indexer (idempotent, keine Selbst-Notifications), `/api/notifications[/count|/seen]`, `/api/followers`, `/api/following`, eigene `/api/thread`-Route; im Client Notifications-View mit Unread-Badge und klickbare Follower-/Following-Listen im Profil |
|
| 8 Social-Graph + Benachrichtigungen | ✅ done — `notifications`-Tabelle, Schreibpfad im Jetstream-Indexer (idempotent, keine Selbst-Notifications), `/api/notifications[/count|/seen]`, `/api/followers`, `/api/following`, eigene `/api/thread`-Route; im Client Notifications-View mit Unread-Badge und klickbare Follower-/Following-Listen im Profil |
|
||||||
|
| 9 Auth + Performance | ✅ done — AppView prüft Bearer-Tokens (ES256, Schlüssel aus dem neuen `/.well-known/did.json` der PDS, fail closed); Timeline und Notifications nur noch für die eigene DID; CORS-Allowlist statt `Any`; Indizes für Handle-Lookup und Cold-Start-Feed |
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
@@ -81,11 +82,13 @@ Stand zuletzt gegen den lokalen Dev-Stack (docker compose + laufender PDS + AppV
|
|||||||
Rust-Workspace grün (u.a. 27 MST, 24 PDS-Integration, 49 AppView-Lib, 14 AppView-Integration),
|
Rust-Workspace grün (u.a. 27 MST, 24 PDS-Integration, 49 AppView-Lib, 14 AppView-Integration),
|
||||||
Frontend grün. Zwei Vorbehalte:
|
Frontend grün. Zwei Vorbehalte:
|
||||||
|
|
||||||
* Die DB-gestützten Integrationstests sind *fail-open* — ohne erreichbare Postgres/PDS
|
* Die DB-gestützten Tests sind *fail-open*: **ohne `DATABASE_URL_APPVIEW` in der
|
||||||
überspringen sie sich selbst und melden das nur auf stderr. Ein grüner Lauf ohne
|
Umgebung überspringen sie sich selbst** und melden das nur auf stderr. `cargo test
|
||||||
laufenden Stack sagt also weniger, als er aussieht.
|
--workspace` in einer nackten Shell meldet dann grün, ohne sie ausgeführt zu haben —
|
||||||
* Einige Tests hängen am Zustand der Dev-Datenbank; auf einer frischen DB können
|
für einen aussagekräftigen Lauf `set -a; . ./.env; set +a` voranstellen und PDS +
|
||||||
`handle_sync`-Tests abweichen. Wer sie ernst nimmt, prüft sie gegen eine definierte DB.
|
AppView laufen lassen.
|
||||||
|
* Läuft die Auth (Default), holen sich die Integrationstests echte Tokens von der PDS
|
||||||
|
bzw. signieren sie aus `PDS_JWT_SECRET`; ohne erreichbare PDS überspringen sie.
|
||||||
|
|
||||||
`crates/tauri-app/src-tauri` hat ein eigenes `[workspace]` und ist **nicht** Teil des
|
`crates/tauri-app/src-tauri` hat ein eigenes `[workspace]` und ist **nicht** Teil des
|
||||||
Root-Workspace; `cargo test --workspace` von oben erfasst den IPC-Layer nicht.
|
Root-Workspace; `cargo test --workspace` von oben erfasst den IPC-Layer nicht.
|
||||||
@@ -103,8 +106,8 @@ Root-Workspace; `cargo test --workspace` von oben erfasst den IPC-Layer nicht.
|
|||||||
* Die eigene PDS speist **keinen** Firehose (`com.atproto.sync.subscribeRepos` fehlt) —
|
* Die eigene PDS speist **keinen** Firehose (`com.atproto.sync.subscribeRepos` fehlt) —
|
||||||
eigene Records erreichen die AppView nur über den Best-Effort-Push
|
eigene Records erreichen die AppView nur über den Best-Effort-Push
|
||||||
`POST /internal/ingest-commit`.
|
`POST /internal/ingest-commit`.
|
||||||
* Die AppView-Leseschnittstelle hat **keine Auth** und CORS `Any`; bei
|
* `aud` wird beim Token-Check nicht validiert (Signatur, Ablauf, `scope` und
|
||||||
`/api/notifications` sind das erstmals halbwegs private Daten.
|
`sub` schon).
|
||||||
* Notifications werden nie gelöscht: Unlike/Unfollow lässt die Zeile stehen, und der
|
* Notifications werden nie gelöscht: Unlike/Unfollow lässt die Zeile stehen, und der
|
||||||
Dedupe-Key macht sie „einmal pro (Empfänger, Autor, Art, Subject) für immer".
|
Dedupe-Key macht sie „einmal pro (Empfänger, Autor, Art, Subject) für immer".
|
||||||
* Auto-Update ist nur dokumentiert, nicht verdrahtet: niemand ruft `check()` auf, das
|
* Auto-Update ist nur dokumentiert, nicht verdrahtet: niemand ruft `check()` auf, das
|
||||||
@@ -112,8 +115,6 @@ Root-Workspace; `cargo test --workspace` von oben erfasst den IPC-Layer nicht.
|
|||||||
* Reply-Notifications gehen verloren, wenn die Antwort vor ihrem Parent indiziert wird
|
* Reply-Notifications gehen verloren, wenn die Antwort vor ihrem Parent indiziert wird
|
||||||
(kein Nachlauf) — bei Jetstream möglich.
|
(kein Nachlauf) — bei Jetstream möglich.
|
||||||
* `at-blob` spricht MinIO ohne Signature V4 — echtes AWS S3 funktioniert damit nicht.
|
* `at-blob` spricht MinIO ohne Signature V4 — echtes AWS S3 funktioniert damit nicht.
|
||||||
* Die PDS liefert kein `.well-known/did.json`; `describeServer` gibt die DID hart
|
|
||||||
als `did:web:pds.maarcadetweet.local` zurück.
|
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ auf welchem Weg kommt ein Post vom Client bis in die Timeline zurück.
|
|||||||
┌──────────────────────────────────┐ ┌──────────────────────────────┐
|
┌──────────────────────────────────┐ ┌──────────────────────────────┐
|
||||||
│ pds-server (axum, :2583) │ │ appview (axum, :2584) │
|
│ pds-server (axum, :2583) │ │ appview (axum, :2584) │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ /xrpc/com.atproto.* │ │ GET /api/timeline/home │
|
│ /xrpc/com.atproto.* │ │ GET /api/timeline/home 🔒 │
|
||||||
│ /xrpc/app.bsky.actor.profile.* │ │ GET /api/profile[/:handle] │
|
│ /xrpc/app.bsky.actor.profile.* │ │ GET /api/profile[/:handle] │
|
||||||
│ /blob/:cid │ │ GET /api/search │
|
│ /blob/:cid │ │ GET /api/search │
|
||||||
│ /healthz │ │ GET /api/post|thread/*uri │
|
│ /healthz │ │ GET /api/post|thread/*uri │
|
||||||
│ │ │ GET /api/notifications… │
|
│ /.well-known/did.json ──────────┼───┼─▶ Schlüssel für 🔒 │
|
||||||
|
│ │ │ GET /api/notifications… 🔒 │
|
||||||
│ │ │ GET /api/followers|following│
|
│ │ │ GET /api/followers|following│
|
||||||
│ │ │ GET /healthz │
|
│ │ │ GET /healthz │
|
||||||
│ at-lexicon Validierung (160) │ │ │
|
│ at-lexicon Validierung (160) │ │ │
|
||||||
@@ -50,6 +51,11 @@ auf welchem Weg kommt ein Post vom Client bis in die Timeline zurück.
|
|||||||
└──────────────────────────────────────────┘ JetstreamConsumer
|
└──────────────────────────────────────────┘ JetstreamConsumer
|
||||||
```
|
```
|
||||||
|
|
||||||
|
🔒 = Bearer-Token nötig, und der `sub` des Tokens muss der angefragten DID
|
||||||
|
entsprechen. Die AppView verifiziert die ES256-Signatur mit dem öffentlichen
|
||||||
|
Schlüssel, den die PDS in ihrem DID-Dokument veröffentlicht — `PDS_JWT_SECRET`
|
||||||
|
verlässt die PDS nie. Details in [`deployment.md`](deployment.md), Abschnitt 6.
|
||||||
|
|
||||||
Zwei Wege führen in die AppView, und das ist Absicht:
|
Zwei Wege führen in die AppView, und das ist Absicht:
|
||||||
|
|
||||||
1. **Direkter Push (schnell, lokal).** Jeder erfolgreiche Commit auf der PDS
|
1. **Direkter Push (schnell, lokal).** Jeder erfolgreiche Commit auf der PDS
|
||||||
|
|||||||
+76
-21
@@ -287,7 +287,7 @@ Hinweise:
|
|||||||
* Es gibt **keinen** Signal-Handler für graceful Shutdown. `systemctl stop`
|
* Es gibt **keinen** Signal-Handler für graceful Shutdown. `systemctl stop`
|
||||||
beendet den Prozess hart; bei der AppView bedeutet das, dass der letzte
|
beendet den Prozess hart; bei der AppView bedeutet das, dass der letzte
|
||||||
Cursor-Flush nur passiert, wenn der Kanal regulär geschlossen wird —
|
Cursor-Flush nur passiert, wenn der Kanal regulär geschlossen wird —
|
||||||
praktisch also mit bis zu 100 Events Verlust (siehe Abschnitt 8). Das ist
|
praktisch also mit bis zu 100 Events Verlust (siehe Abschnitt 9). Das ist
|
||||||
unkritisch, weil der Cursor beim Resume ohnehin leicht in die Vergangenheit
|
unkritisch, weil der Cursor beim Resume ohnehin leicht in die Vergangenheit
|
||||||
zeigt und Events idempotent verarbeitet werden.
|
zeigt und Events idempotent verarbeitet werden.
|
||||||
* Eine Abhängigkeit `After=` auf Postgres/MinIO ist nur nötig, wenn diese auf
|
* Eine Abhängigkeit `After=` auf Postgres/MinIO ist nur nötig, wenn diese auf
|
||||||
@@ -296,7 +296,55 @@ Hinweise:
|
|||||||
`PgPoolOptions` mit `acquire_timeout(10s)`, bricht aber ebenfalls ab, wenn
|
`PgPoolOptions` mit `acquire_timeout(10s)`, bricht aber ebenfalls ab, wenn
|
||||||
der erste Connect scheitert. `Restart=on-failure` fängt das ab.
|
der erste Connect scheitert. `Restart=on-failure` fängt das ab.
|
||||||
|
|
||||||
## 6. Reverse-Proxy
|
## 6. Authentifizierung
|
||||||
|
|
||||||
|
Die AppView prüft seit Phase 9 Bearer-Tokens. Wie das zusammenhängt:
|
||||||
|
|
||||||
|
1. Die PDS stellt beim Login ein ES256-Access-JWT aus (`sub` = DID,
|
||||||
|
`scope` = `com.atproto.access`, eine Stunde gültig).
|
||||||
|
2. Die PDS veröffentlicht den *öffentlichen* Teil ihres P-256-Schlüssels unter
|
||||||
|
`GET /.well-known/did.json`. `PDS_JWT_SECRET` verlässt den PDS-Prozess nicht.
|
||||||
|
3. Die AppView holt dieses Dokument beim Start von `PDS_INTERNAL_URL`
|
||||||
|
(Fallback: `PDS_PUBLIC_URL`), cached den Schlüssel und lädt ihn bei einem
|
||||||
|
Verifikationsfehler einmal nach — höchstens einmal pro Minute, damit
|
||||||
|
Müll-Tokens kein Werkzeug werden, die PDS zu fluten. Ein Schlüsselwechsel
|
||||||
|
braucht also keinen Neustart der AppView.
|
||||||
|
|
||||||
|
Ist die PDS beim Start nicht erreichbar, warnt die AppView nur und startet
|
||||||
|
trotzdem — sie indiziert den Firehose, was von der lokalen PDS unabhängig ist.
|
||||||
|
Der Schlüssel wird dann beim ersten authentifizierten Request geholt. Klappt
|
||||||
|
auch das nicht, antwortet sie `503 AuthUnavailable`: **fail closed**, nie
|
||||||
|
fail open.
|
||||||
|
|
||||||
|
### Welche Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Zugriff |
|
||||||
|
|---|---|
|
||||||
|
| `/api/timeline/home`, `/api/notifications`, `/api/notifications/count`, `/api/notifications/seen` | Token nötig, `sub` muss dem `did`-Parameter entsprechen |
|
||||||
|
| `/api/profile*`, `/api/search`, `/api/post/*`, `/api/thread*`, `/api/followers`, `/api/following` | öffentlich (in AT Proto öffentliche Records) |
|
||||||
|
| `/internal/ingest-commit` | `APPVIEW_INGEST_SECRET`, server-zu-server |
|
||||||
|
|
||||||
|
### Fehlercodes
|
||||||
|
|
||||||
|
| Fall | Status | `error` |
|
||||||
|
|---|---|---|
|
||||||
|
| Header fehlt oder ist kein Bearer | 401 | `AuthMissing` |
|
||||||
|
| Signatur falsch, abgelaufen, falscher `scope` | 401 | `TokenInvalid` |
|
||||||
|
| Token gültig, aber `sub` ≠ `did` | 403 | `Forbidden` |
|
||||||
|
| Schlüssel der PDS nicht beschaffbar | 503 | `AuthUnavailable` |
|
||||||
|
|
||||||
|
`TokenInvalid` ist ein Vertrag mit dem Desktop-Client: daran erkennt er, dass
|
||||||
|
er sein Access-JWT erneuern und den Request einmal wiederholen muss. Wer den
|
||||||
|
Code umbenennt, loggt jeden Nutzer eine Stunde nach dem Login aus.
|
||||||
|
|
||||||
|
### `APPVIEW_AUTH_REQUIRED=false`
|
||||||
|
|
||||||
|
Schaltet die Prüfung ab und stellt das alte Verhalten her — gedacht für eine
|
||||||
|
Instanz hinter VPN und für die fail-open-Integrationstests. Die AppView warnt
|
||||||
|
beim Start in Großbuchstaben. Öffentlich erreichbar heißt das: jeder kann die
|
||||||
|
Notifications jeder DID lesen und als gelesen markieren.
|
||||||
|
|
||||||
|
## 7. Reverse-Proxy
|
||||||
|
|
||||||
### PDS
|
### PDS
|
||||||
|
|
||||||
@@ -357,25 +405,31 @@ im DID-Doc-`serviceEndpoint`, im JWT-`iss` und als Basis der Blob-URLs.
|
|||||||
kein `/.well-known/atproto-did` aus (im Router nicht vorhanden). `at-identity`
|
kein `/.well-known/atproto-did` aus (im Router nicht vorhanden). `at-identity`
|
||||||
kann solche Dokumente *auflösen*, aber wer `did:web`-Handles auf dieser PDS
|
kann solche Dokumente *auflösen*, aber wer `did:web`-Handles auf dieser PDS
|
||||||
betreiben will, muss die Dateien vorerst statisch über den Proxy ausliefern.
|
betreiben will, muss die Dateien vorerst statisch über den Proxy ausliefern.
|
||||||
Ebenfalls offen: `describeServer` gibt die DID hart als
|
Die Service-DID wird inzwischen aus `PDS_PUBLIC_URL` abgeleitet
|
||||||
`did:web:pds.maarcadetweet.local` zurück, unabhängig von `PDS_PUBLIC_URL`.
|
(`AppConfig::pds_did()`, did:web mit `%3A`-kodiertem Port) und von
|
||||||
|
`describeServer` **und** `/.well-known/did.json` identisch ausgeliefert. Das
|
||||||
|
heißt auch: ändert sich `PDS_PUBLIC_URL`, ändert sich die Service-DID.
|
||||||
|
|
||||||
### AppView
|
### AppView
|
||||||
|
|
||||||
Die AppView setzt ihr CORS selbst — `crates/appview/src/routes.rs`:
|
Die AppView setzt ihr CORS selbst (`cors_layer()` in
|
||||||
|
`crates/appview/src/routes.rs`). Ohne `APPVIEW_CORS_ORIGINS` bleibt es beim
|
||||||
|
alten `Access-Control-Allow-Origin: *` — die AppView warnt dann beim Start.
|
||||||
|
Mit gesetzter Variable gilt eine Allowlist:
|
||||||
|
|
||||||
```rust
|
```
|
||||||
let cors = CorsLayer::new()
|
APPVIEW_CORS_ORIGINS=tauri://localhost,http://tauri.localhost,http://127.0.0.1:1430
|
||||||
.allow_origin(Any)
|
|
||||||
.allow_methods(Any)
|
|
||||||
.allow_headers(Any);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Also `Access-Control-Allow-Origin: *` für alle Routen, inklusive
|
Der Tauri-Webview ruft die AppView von einem anderen Origin aus auf — im Dev
|
||||||
`/internal/ingest-commit`. Der Grund steht im Code: der Tauri-Webview ruft die
|
die Vite-Adresse, im Bundle `tauri://localhost` (macOS/Linux) bzw.
|
||||||
AppView von einem anderen Origin aus auf (`http://127.0.0.1:1430` im Dev,
|
`http://tauri.localhost` (Windows). Alle drei gehören in die Liste, sonst
|
||||||
`tauri://` / `asset://` im Bundle), und die Read-Endpoints tragen keine
|
scheitert der Preflight.
|
||||||
Auth-Cookies.
|
|
||||||
|
`/internal/ingest-commit` liegt bewusst **außerhalb** der CORS-Schicht: die
|
||||||
|
Route wird von der PDS server-zu-server aufgerufen, nie von einem Browser. Ein
|
||||||
|
`Access-Control-Allow-Origin` darauf würde ausschließlich einer Webseite
|
||||||
|
helfen, in den Index zu schreiben.
|
||||||
|
|
||||||
Für ein öffentliches Deployment heißt das:
|
Für ein öffentliches Deployment heißt das:
|
||||||
|
|
||||||
@@ -408,7 +462,7 @@ HTTP-Aufrufe an PDS/AppView laufen über den Rust-IPC-Layer
|
|||||||
(`src-tauri/src/pds_client.rs`, `appview_client.rs`), nicht aus dem Webview —
|
(`src-tauri/src/pds_client.rs`, `appview_client.rs`), nicht aus dem Webview —
|
||||||
die CSP muss also für neue Backend-URLs nicht angefasst werden.
|
die CSP muss also für neue Backend-URLs nicht angefasst werden.
|
||||||
|
|
||||||
## 7. Health-Checks und Logs
|
## 8. Health-Checks und Logs
|
||||||
|
|
||||||
### PDS
|
### PDS
|
||||||
|
|
||||||
@@ -472,7 +526,7 @@ Log-Zeilen, auf die es sich lohnt zu achten:
|
|||||||
| `s3 ping failed at startup` | MinIO beim PDS-Start nicht erreichbar |
|
| `s3 ping failed at startup` | MinIO beim PDS-Start nicht erreichbar |
|
||||||
| `plc submit failed (dev ok)` | PLC-Directory nicht erreichbar; die DID bleibt lokal gültig, ist aber global nicht registriert |
|
| `plc submit failed (dev ok)` | PLC-Directory nicht erreichbar; die DID bleibt lokal gültig, ist aber global nicht registriert |
|
||||||
|
|
||||||
## 8. Neustart-Verhalten
|
## 9. Neustart-Verhalten
|
||||||
|
|
||||||
**PDS.** Zustandslos bis auf Postgres und MinIO. Der In-Memory-Blockstore
|
**PDS.** Zustandslos bis auf Postgres und MinIO. Der In-Memory-Blockstore
|
||||||
(`MemoryBlockstore` in `state.rs`) wird beim Start neu aufgebaut; persistent
|
(`MemoryBlockstore` in `state.rs`) wird beim Start neu aufgebaut; persistent
|
||||||
@@ -515,14 +569,15 @@ nach — zuerst über die lokale PDS (`PdsHandleResolver`, 2 s Timeout), dann PL
|
|||||||
bzw. `did:web`. Nach einem Neustart holt der erste Durchlauf das nach; der
|
bzw. `did:web`. Nach einem Neustart holt der erste Durchlauf das nach; der
|
||||||
Zustand ist reine Anzeigekosmetik.
|
Zustand ist reine Anzeigekosmetik.
|
||||||
|
|
||||||
## 9. Was noch offen ist
|
## 10. Was noch offen ist
|
||||||
|
|
||||||
* Kein Compose-Service für `pds-server` / `appview` — das Compose-File deckt nur
|
* Kein Compose-Service für `pds-server` / `appview` — das Compose-File deckt nur
|
||||||
Postgres und MinIO ab. Es gibt kein Dockerfile im Repo.
|
Postgres und MinIO ab. Es gibt kein Dockerfile im Repo.
|
||||||
* `at-blob` spricht ausschließlich MinIO ohne Signature V4 (siehe Modul-Doku in
|
* `at-blob` spricht ausschließlich MinIO ohne Signature V4 (siehe Modul-Doku in
|
||||||
`crates/at-blob/src/s3.rs`); echtes AWS S3 funktioniert damit nicht.
|
`crates/at-blob/src/s3.rs`); echtes AWS S3 funktioniert damit nicht.
|
||||||
* Kein Graceful-Shutdown, keine Readiness- (im Unterschied zur Liveness-)Probe.
|
* Kein Graceful-Shutdown, keine Readiness- (im Unterschied zur Liveness-)Probe.
|
||||||
* Keine konfigurierbare CORS-Allowlist in der AppView.
|
* `aud` wird beim Token-Check nicht validiert (`verify_jwt` setzt
|
||||||
* Kein `.well-known`-Handling in der PDS, `describeServer` liefert eine
|
`validate_aud = false`), obwohl die PDS `did:web:appview.maarcadetweet.local`
|
||||||
hartkodierte DID.
|
einsetzt. Signatur, Ablauf, `scope` und `sub` werden geprüft.
|
||||||
|
* Notifications werden nie gelöscht; ein Unlike/Unfollow lässt die Zeile stehen.
|
||||||
* Kein Backfill-Werkzeug für Jetstream-Lücken.
|
* Kein Backfill-Werkzeug für Jetstream-Lücken.
|
||||||
|
|||||||
Reference in New Issue
Block a user