fix: update atproto SDK for permission set scope validation (#698)

Updates the atproto dependency to include the fix for OAuth scope
validation when using permission sets. The PDS expands `include:`
scopes into `repo?collection=` format, which the SDK now handles.

Also updates docs with correct DNS setup (`_lexicon` prefix, not `_atproto`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

authored by zzstoatzz.io Claude Opus 4.5 and committed by GitHub 1e8ebdb4 802ee1e4

Changed files
+69 -4
backend
docs
scripts
+2 -2
backend/uv.lock
··· 287 288 [[package]] 289 name = "atproto" 290 - version = "0.0.1.dev470" 291 - source = { git = "https://github.com/zzstoatzz/atproto?rev=main#6710edb64d144a0ff2757526ade34fa1625a46e6" } 292 dependencies = [ 293 { name = "click" }, 294 { name = "cryptography" },
··· 287 288 [[package]] 289 name = "atproto" 290 + version = "0.0.1.dev471" 291 + source = { git = "https://github.com/zzstoatzz/atproto?rev=main#0de1a49c6592ae2c3193948bda1b3a0861316cb5" } 292 dependencies = [ 293 { name = "click" }, 294 { name = "cryptography" },
+6 -2
docs/research/2026-01-01-atproto-oauth-permission-sets.md
··· 128 ) 129 ``` 130 131 - **DNS requirement:** `plyr.fm` must have a TXT record `did=did:plc:...` pointing to the repo containing the lexicons. 132 133 ### official bluesky permission sets 134 ··· 177 178 ## resolved questions 179 180 - 1. **DNS setup**: `_atproto.plyr.fm` already has TXT record `did=did:plc:vs3hnzq2daqbszxlysywzy54` 181 182 2. **which repo?**: the `plyr.fm` account (did:plc:vs3hnzq2daqbszxlysywzy54) on bsky.network - just publish to `com.atproto.lexicon.schema` collection 183
··· 128 ) 129 ``` 130 131 + **DNS requirement:** lexicon resolution uses `_lexicon` prefix (distinct from `_atproto` for handles): 132 + - `_lexicon.plyr.fm` → `did=did:plc:vs3hnzq2daqbszxlysywzy54` (production) 133 + - `_lexicon.stg.plyr.fm` → `did=did:plc:vs3hnzq2daqbszxlysywzy54` (staging) 134 135 ### official bluesky permission sets 136 ··· 179 180 ## resolved questions 181 182 + 1. **DNS setup**: lexicon resolution requires `_lexicon` TXT records (not `_atproto` which is for handles): 183 + - production: `_lexicon.plyr.fm` → `did=did:plc:vs3hnzq2daqbszxlysywzy54` 184 + - staging: `_lexicon.stg.plyr.fm` → `did=did:plc:vs3hnzq2daqbszxlysywzy54` 185 186 2. **which repo?**: the `plyr.fm` account (did:plc:vs3hnzq2daqbszxlysywzy54) on bsky.network - just publish to `com.atproto.lexicon.schema` collection 187
+61
scripts/publish_permission_set.py
···
··· 1 + #!/usr/bin/env python 2 + """publish permission set lexicon to ATProto repo with specific rkey.""" 3 + 4 + import asyncio 5 + import os 6 + 7 + from atproto import AsyncClient 8 + 9 + 10 + async def main(): 11 + handle = os.environ["PLYRFM_HANDLE"] 12 + password = os.environ["PLYRFM_PASSWORD"] 13 + namespace = os.environ.get("NAMESPACE", "fm.plyr") 14 + 15 + client = AsyncClient() 16 + await client.login(handle, password) 17 + 18 + permission_set_id = f"{namespace}.authFullApp" 19 + 20 + record = { 21 + "$type": "com.atproto.lexicon.schema", 22 + "lexicon": 1, 23 + "id": permission_set_id, 24 + "defs": { 25 + "main": { 26 + "type": "permission-set", 27 + "title": "plyr.fm", 28 + "description": "Upload and manage audio content, playlists, likes, and comments.", 29 + "permissions": [ 30 + { 31 + "type": "permission", 32 + "resource": "repo", 33 + "action": ["create", "update", "delete"], 34 + "collection": [ 35 + f"{namespace}.track", 36 + f"{namespace}.like", 37 + f"{namespace}.comment", 38 + f"{namespace}.list", 39 + f"{namespace}.actor.profile", 40 + ], 41 + } 42 + ], 43 + } 44 + }, 45 + } 46 + 47 + result = await client.com.atproto.repo.put_record( 48 + { 49 + "repo": client.me.did, 50 + "collection": "com.atproto.lexicon.schema", 51 + "rkey": permission_set_id, 52 + "record": record, 53 + } 54 + ) 55 + 56 + print(f"created: {result.uri}") 57 + print(f"cid: {result.cid}") 58 + 59 + 60 + if __name__ == "__main__": 61 + asyncio.run(main())