Der Client legt Follows über createRecord mit app.bsky.graph.follow an. Das
Lexicon war in der PDS aber nicht registriert, und createRecord validiert
per Default — jede Anfrage kam mit
400 lex validation failed: unknown lexicon: app.bsky.graph.follow
zurück. Der Follow-Button kann also nie funktioniert haben, auch wenn der
Commit, der ihn eingeführt hat, "end-to-end follow / unfollow" heißt. Beim
Gegenprüfen des Firehose-Pfads aufgefallen: der Testaufbau scheiterte schon
am Anlegen des Follows.
Das Lexicon ist jetzt da (subject als DID-String, nicht als strongRef —
genau das, was der Client schickt und was follow_subject_did in der AppView
liest) und registriert. Live geprüft: anlegen, in der AppView indiziert,
löschen, Zeile weg.
Dazu ein zweiter Grund, warum das nie auffiel: create_record_with nahm einen
Parameter `_validate` entgegen und verwarf ihn. Der eine Aufrufer, der
`false` übergab, bekam trotzdem Validierung. Der Parameter wird jetzt
tatsächlich mitgeschickt; der Repost-Pfad steht auf `true`, weil genau das
bisher schon passiert ist und funktioniert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
Read / read-modify-write the authenticated user's profile record
through the standard atproto repo-write path. Auth is checked via
the existing bearer-token helper; the request body's
`display_name` / `description` / `avatar_blob_cid` /
`banner_blob_cid` overlay the existing record (None fields
preserve the old value).
Blob CID ownership: any supplied avatar/banner CID is looked up
in the `blobs` table with `WHERE cid = $1 AND did = $2`,
rejecting with 400 if the blob isn't owned by the authenticated
user. The resolved `mime_type` / `size` is written into the
record so consumers reading `size` for layout decisions get the
real value (previously hardcoded to "image/png" / 0).
Best-effort push to the AppView via `AppViewPushClient::push_profile`
so the `profiles` cache reflects the new avatar / display name
without waiting for the Jetstream replay path.
Wire shape:
GET /xrpc/app.bsky.actor.profile.get
→ { did, handle, profile: { displayName, description, ... } | null }
POST /xrpc/app.bsky.actor.profile.set
body: { displayName, description, avatarBlobCid, bannerBlobCid }
→ same shape as get
Includes `merge_profile_fields` testable helper (4 unit tests
locking the camelCase wire shape and the merge semantics).
The AppView-side indexer arm and the Tauri UI land in the
following two commits.
Three fixes for the integration test plan:
1. Layer order in tauri.conf.json: `body_limit_fallback` must
wrap `upload_blob_body_limit` so the JSON override is in
effect when the 413 fires. Swapped.
2. Type annotation on the `from_fn` middleware:
`.layer::<_, std::convert::Infallible>(...)`. The function
never errors, so the second type param is Infallible.
3. Register the standard `app.bsky.feed.like` and
`app.bsky.feed.repost` lexicons so the like/repost
endpoints (which create records of those collections) pass
the lex validator. We only ship what the PDS actually lets
users create server-side; anything else passes `validate: false`.
The 'unprocessable entity' style message and 'unknown lexicon'
errors that came up during manual testing are now gone.
Also dropped the stuck migration-2 row from `_sqlx_migrations`
on the dev DB so the new lex schemas apply.