Identities for entities did.bot

feat(serve)!: store app.bsky preferences instead of answering 501 #2554

Open permadeath.com
targeting main from claude/bsky-preferences
Open opened by and assigned to permadeath.com
targeting main from claude/bsky-preferences

app.bsky.actor.putPreferences now stores the account's preferences instead of answering 501, and getPreferences reads them back, so settings saved in bsky.app stick. Both are served here and never proxied. A put replaces the list whole. Every item needs a $type in the app.bsky namespace (400 InvalidRequest otherwise). A list over [capacity] max_preferences_bytes (64 KiB by default, 8 KiB to 1 MiB) is 413 PreferencesTooLarge. A frozen or otherwise unwritable account is 403 AccountNotWritable. The account's own agent token reaches both. An OAuth token needs transition:generic, which the OAuth spec says grants "read and write any personal preferences", or an rpc: scope for the method at the app view. That second path matches how Bluesky's app.bsky.authFullApp permission set grants these methods, and it is the only path that works here, since the scope ceiling never grants transition:generic.

New state for review. Each account gets one optional, opaque JSON string: the preferences array exactly as serialized at the last put. It lives in MemoryAccountStore beside the row and signing key, not in HostedAccount, so listings and logs never carry it. A new log entry, accountPreferencesSet { did, preferences: string | null }, records it. Each put writes one entry (synced). An erasure writes null, and a hard delete removes it with the row. The checkpoint writes one entry per account that has preferences stored. The data layout moves to 24 and still reads 23. The volume backups carry the log, and a CAR export does not include preferences, the same as the reference PDS. Every account's preferences are held in memory, so the cap times max_accounts is the most they can take.

Closes #225.

🤖 Generated with Claude Code

Commits 3

feat(serve): serve app.bsky.actor.getPreferences and putPreferences
feat(serve): cap each account's preferences with a config key
feat(pds)!: keep an account's app.bsky preferences in the log