audio streaming app plyr.fm

docs: document developer token scoping and fix stale scope references (#893)

the developer tokens docs incorrectly stated "tokens have full account
access - treat like passwords." in reality, tokens are scoped to
plyr.fm's lexicon namespace (fm.plyr.*) via ATProto OAuth and the PDS
enforces this at the protocol level. also fixes stale resolved_scope
examples in configuration docs that were missing blob, comment, list,
and profile collections.

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

authored by zzstoatzz.io

Claude Opus 4.6 and committed by
GitHub
14e72933 62f5592f

+31 -21
+12 -13
docs/authentication.md
··· 458 458 459 459 ### how it works 460 460 461 - developer tokens are sessions with their own independent OAuth grant. when you create a dev token, you go through a full OAuth authorization flow at your PDS, which gives the token its own access/refresh credentials. this means: 461 + developer tokens are sessions with their own independent OAuth grant — **not app passwords**. when you create a dev token, you go through a full OAuth authorization flow at your PDS, which gives the token its own access/refresh credentials. this means: 462 462 - dev tokens can refresh independently (no staleness when browser session refreshes) 463 463 - each token has its own DPoP keypair for request signing 464 - - logging out of browser doesn't affect dev tokens (cookie isolation) 465 - - revoking browser session doesn't affect dev tokens 464 + - browser logout/revocation doesn't affect dev tokens (cookie isolation) 465 + 466 + ### scoping 467 + 468 + developer tokens are **scoped to plyr.fm's lexicon namespace** via ATProto OAuth. the grant requests only the collections plyr.fm needs (e.g. `atproto blob:*/* include:fm.plyr.authFullApp` via [permission sets](https://atproto.com/specs/oauth#permission-sets), or granular `repo:fm.plyr.track repo:fm.plyr.like ...` scopes as fallback). 469 + 470 + - **can** read/write plyr.fm data (tracks, likes, comments, playlists, profile) and upload blobs 471 + - **cannot** read or write your Bluesky posts, follows, blocks, or any other app's data 472 + - **cannot** modify your ATProto identity or account settings 466 473 467 - dev tokens can: 468 - - read your data (tracks, likes, profile) 469 - - upload tracks (creates ATProto records on your PDS) 470 - - perform any authenticated action 474 + the PDS enforces these scopes at the protocol level — not just plyr.fm's API. rather than app passwords (full repo access, coupled to Bluesky), plyr.fm issues OAuth grants scoped to the minimum permissions needed. 471 475 472 - **security notes**: 473 - - tokens have full account access - treat like passwords 474 - - revoke individual tokens via the portal or API 475 - - each token is independent - revoking one doesn't affect others 476 - - token names help identify which token is used where 477 - - tokens require explicit OAuth consent at your PDS 476 + **security notes**: tokens grant full access to plyr.fm features, but are namespace-scoped at the protocol level. each token is independent — revoke individually via the portal or API. 478 477 479 478 ## OAuth client types: public vs confidential 480 479
+19 -8
docs/backend/configuration.md
··· 150 150 151 151 ### `settings.atproto.resolved_scope` 152 152 153 - constructs the oauth scope from the collection(s): 153 + constructs the OAuth scope from the app's lexicon collections. this scope is used for all OAuth grants — browser sessions and [developer tokens](../authentication.md#scoping) alike — ensuring tokens can only access plyr.fm's namespace on the user's PDS. 154 + 154 155 ```python 155 - # base scopes: our track collection + our like collection 156 + # with permission sets (default when lexicons are published): 157 + "atproto blob:*/* include:fm.plyr.authFullApp" 158 + 159 + # fallback: granular repo scopes for each collection 156 160 scopes = [ 161 + "blob:*/*", 157 162 f"repo:{settings.atproto.track_collection}", 158 - f"repo:{settings.atproto.app_namespace}.like", 163 + f"repo:{settings.atproto.like_collection}", 164 + f"repo:{settings.atproto.comment_collection}", 165 + f"repo:{settings.atproto.list_collection}", 166 + f"repo:{settings.atproto.profile_collection}", 159 167 ] 160 168 161 169 # if we have an old namespace, add old track collection too ··· 163 171 scopes.append(f"repo:{settings.atproto.old_track_collection}") 164 172 165 173 return f"atproto {' '.join(scopes)}" 166 - # default: "atproto repo:fm.plyr.track repo:fm.plyr.like" 174 + # default: "atproto blob:*/* repo:fm.plyr.track repo:fm.plyr.like repo:fm.plyr.comment repo:fm.plyr.list repo:fm.plyr.actor.profile" 167 175 ``` 168 176 169 177 can be overridden with `ATPROTO_SCOPE_OVERRIDE` if needed. ··· 178 186 179 187 this defines the collections: 180 188 - `track_collection` → `"fm.plyr.track"` 181 - - `like_collection` → `"fm.plyr.like"` (implicit) 182 - - `resolved_scope` → `"atproto repo:fm.plyr.track repo:fm.plyr.like"` 189 + - `like_collection` → `"fm.plyr.like"` 190 + - `comment_collection` → `"fm.plyr.comment"` 191 + - `list_collection` → `"fm.plyr.list"` 192 + - `profile_collection` → `"fm.plyr.actor.profile"` 193 + - `resolved_scope` → `"atproto blob:*/* include:fm.plyr.authFullApp"` (with permission sets) 183 194 184 195 ### environment-specific namespaces 185 196 ··· 224 235 ATPROTO_OLD_APP_NAMESPACE=app.relay # optional, for migration 225 236 ``` 226 237 227 - when set, OAuth scopes will include both old and new namespaces: 238 + when set, OAuth scopes will include the old track collection alongside current collections: 228 239 - `old_track_collection` → `"app.relay.track"` 229 - - `resolved_scope` → `"atproto repo:fm.plyr.track repo:fm.plyr.like repo:app.relay.track"` 240 + - `resolved_scope` includes `repo:app.relay.track` in addition to all `fm.plyr.*` scopes 230 241 231 242 ## usage in code 232 243