Mit gesetztem DATABASE_URL_APPVIEW liefen diese Tests zum ersten Mal
überhaupt (ohne die Variable überspringen sie sich still) — und fielen
um. Zwei Ursachen:
1. Sechs Integrationstests hingen wie zuvor die Unit-Tests am globalen
run_once()-Batch. select_candidates/resolve_batch sind dafür jetzt
pub, damit auch die Integrationstests ihren eigenen DID durchreichen
können statt zu hoffen, dass er es in den Batch schafft.
2. Die Dispatch-Tests für did:web und did:plc verdrahteten den fremden
Stub als pds_resolver — also eine lokale PDS, die behauptet, eine
fremde DID zu kennen. Die Moduldoku sagt ausdrücklich, dass die PDS
vor der Methodenverzweigung befragt wird, damit ein did:key-Nutzer
der eigenen PDS ohne Umweg über plc.directory auflöst. Die Fixtures
haben also gegen die dokumentierte Regel getestet statt gegen die
Verzweigung, um die es ihnen ging. Jetzt kennt die PDS-Stub die DID
nicht, wie es der Realität entspricht.
Neu: pds_resolves_did_key_before_method_dispatch pinnt die PDS-zuerst-
Regel selbst — dasselbe DID-Verfahren, umgekehrtes Ergebnis, und der
Unterschied ist allein, ob die PDS den Nutzer hostet.
sync_skips_already_resolved prüft weiter über select_candidates: dass
ein DID mit Handle gar nicht erst bei einem Resolver landet, ist der
Punkt des Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
Die AppView hatte keinerlei Authentifizierung: jeder konnte
/api/notifications?did=<beliebig> lesen und per /seen als gelesen
markieren. Mit Phase 8 sind das die ersten privaten Daten im System.
Das Access-JWT der PDS trug von Anfang an sub, scope
"com.atproto.access" und aud "did:web:appview…" — es war für die
AppView ausgestellt, nur hat sie es nie geprüft. Neu ist deshalb vor
allem die Schlüsselbeschaffung: auth.rs holt das DID-Dokument der PDS
(PDS_INTERNAL_URL, sonst PDS_PUBLIC_URL), cached den Schlüssel und lädt
ihn bei einem Verifikationsfehler nach — höchstens einmal pro Minute,
damit Müll-Tokens kein Werkzeug werden, die PDS zu fluten. Ein
Schlüsselwechsel braucht damit keinen Neustart.
Ist die PDS beim Start weg, warnt die AppView nur und startet trotzdem
(sie indiziert den Firehose, der von der lokalen PDS unabhängig ist).
Ist der Schlüssel beim Prüfen eines Tokens nicht zu beschaffen, gibt es
503 — fail closed.
Geschützt: /api/timeline/home und die drei Notification-Endpoints, jeweils
mit sub == did. Öffentlich bleiben Profile, Suche, Posts, Threads und die
Follower-Listen; das sind in AT Proto öffentliche Records.
401 AuthMissing / 401 TokenInvalid / 403 Forbidden / 503 AuthUnavailable.
TokenInvalid ist ein Vertrag mit dem Client: daran erkennt er, dass er
sein Token erneuern und einmal wiederholen muss.
Dazu CORS: statt Any für alles jetzt eine Allowlist über
APPVIEW_CORS_ORIGINS (unset = altes Verhalten plus Warnung), und
/internal/ingest-commit liegt außerhalb der CORS-Schicht — die Route
wird server-zu-server aufgerufen, ein Allow-Origin darauf würde nur
einer Webseite helfen, in den Index zu schreiben.
APPVIEW_AUTH_REQUIRED=false stellt das alte Verhalten her (VPN-Instanz,
fail-open-Tests) und warnt beim Start in Großbuchstaben.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
Bisher erfuhr ein Nutzer nie, dass jemand anderes mit ihm interagiert hat:
Like, Repost, Follow und Reply hinterließen keine Spur, an der der Client
hätte pollen können. Der Tray-/Notification-Pfad im Desktop-Client (Phase 7)
hing damit in der Luft.
Migration 0008:
* notifications(recipient, author, kind, subject_uri, created_at,
indexed_at, read_at) mit Keyset-Index (recipient, indexed_at DESC, id DESC)
und Partial-Index auf ungelesene Zeilen für den Badge-Poll.
* Dedupe-Unique-Index über COALESCE(subject_uri, '') — plain NULLs
kollidieren nicht, sonst gäbe es pro Follow beliebig viele Zeilen.
Folge: Unlike-Relike erzeugt keine zweite Notification, Toggle-Spam ist
damit ausgeschlossen.
* Bewusst kein CHECK (recipient <> author): ein Ausrutscher dort würde die
umgebende Like-Transaktion abbrechen, also das Like wegen eines
Notification-Bugs verlieren. Gefiltert wird in Rust und im INSERT.
Indexer: record_notification() hängt an upsert_like/-repost (in derselben
Transaktion wie die Counter) sowie upsert_follow/-post. Selbst-Interaktionen
sind still. Empfänger muss uns bekannt sein (profiles- oder posts-Zeile),
sonst würden wir für den gesamten öffentlichen Firehose Zeilen anlegen —
als ein INSERT ... SELECT ... WHERE EXISTS, also ohne TOCTOU-Fenster.
Reply-Notifications tragen die URI der *Antwort* als subject_uri, weil die
Liste den Text zeigt, den der Empfänger noch nicht kennt.
Endpoints: GET /api/notifications, /api/notifications/count,
POST /api/notifications/seen (seenAt als Wasserzeichen),
GET /api/followers, /api/following, GET /api/thread (beide Schreibweisen).
Cursor-Codec, Limit-Clamping und Fehlerform sind die der bestehenden
Endpoints.
/api/post/*uri bleibt wire-kompatibel und teilt sich jetzt
load_thread_context() mit /api/thread — mit max_parents = 1, weil es nur
den direkten Parent serialisiert; die volle Ahnenkette wären bis zu 20
sequenzielle Queries für Zeilen, die danach verworfen werden.
Nebenbei ein Darstellungsfehler: der synthetische Platzhalter-Handle für
Actors ohne bekannten Handle trug ein führendes '@', während jeder Consumer
selbst '@{handle}' rendert — im Feed kam '@@did:plc:abcd…' heraus. Der
Platzhalter ist jetzt durchgängig sigil-frei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
The AppView's handle_sync worker consulted the public PLC directory
and the did:web: HTTPS resolver only. DIDs hosted on the local PDS
(notably did🔑 users and any other operator-hosted method)
weren't reachable without an external round trip, and unresolvable
DIDs (did🔑 not on this PDS, did:foo: anything) blocked the
100-row batch forever because did🔑 sorts lexicographically
before did:plc: / did:web:.
This commit adds:
* `PdsHandleResolver` (at-identity) — POSTs the DID as the
`handle` field to the PDS's resolveHandle XRPC method. The PDS
now recognises a `did:` prefix and does a PK lookup on
`users.did`, returning `{did, handle}`. The resolver reads
the `handle` field, so the AppView finally gets a real local
handle for did🔑 users without ever dialing plc.directory.
* A 2 s timeout per request (was 10 s) and `DISPATCH_CONCURRENCY =
8` so the worker caps a 100-DID batch at ~2 s with parallel
dispatch instead of the ~17 min worst case the old serial + 10 s
setup allowed.
* A new `posts.handle_sync_attempted_at` column (migration 0006)
and `mark_attempted()` helper. The SELECT filter excludes rows
attempted within the last hour, so an unresolvable DID dominates
at most one batch before the worker advances. Cleared on success.
* `PDS_INTERNAL_URL` config so the AppView can reach the PDS via
a cluster-internal hostname when the public URL isn't routable
from inside the cluster.
Tests:
* `crates/at-identity/src/pds_handle.rs` — 4 unit tests against a
stub HTTP server (200/404/5xx/missing-did-field).
* Existing handle_sync integration tests updated to wire in the
new `pds_resolver` field.