Scope Validation Comparison: pds.js vs atproto PDS#
Comparison of OAuth scope validation between this implementation and the official AT Protocol PDS.
Scope Types Supported#
| Scope Type |
Format |
pds.js |
atproto PDS |
atproto |
Static |
Checked (loose) |
Required for all OAuth |
transition:generic |
Static |
Not recognized |
Full repo/blob bypass |
transition:email |
Static |
N/A |
Read account email |
transition:chat.bsky |
Static |
N/A |
Chat RPC access |
repo:<collection>:<action> |
Granular |
Not parsed |
Full parsing + enforcement |
blob:<mime> |
Granular |
Not parsed |
Full parsing + enforcement |
rpc:<aud>:<lxm> |
Granular |
Not parsed |
Full parsing + enforcement |
Scope Enforcement by Endpoint#
com.atproto.repo.createRecord#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
hasRequiredScope(scope, 'atproto') |
permissions.assertRepo({ action: 'create', collection }) |
| Required scope |
atproto anywhere in scope string |
repo:<collection>:create or transition:generic or atproto |
| OAuth-only check |
No (checks all tokens) |
Yes (legacy Bearer bypasses) |
| Error response |
403 "Insufficient scope for repo write" |
403 "Missing required scope "repo:..."" |
com.atproto.repo.putRecord#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
hasRequiredScope(scope, 'atproto') |
assertRepo({ action: 'create' }) AND assertRepo({ action: 'update' }) |
| Required scope |
atproto |
Both repo:<collection>:create AND repo:<collection>:update |
| Notes |
Single check |
Requires both since putRecord can create or update |
com.atproto.repo.deleteRecord#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
hasRequiredScope(scope, 'atproto') |
permissions.assertRepo({ action: 'delete', collection }) |
| Required scope |
atproto |
repo:<collection>:delete |
com.atproto.repo.applyWrites#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
hasRequiredScope(scope, 'atproto') |
Iterates all writes, asserts each unique action/collection pair |
| Required scope |
atproto |
All repo:<collection>:<action> for each write |
| Per-write validation |
No |
Yes |
com.atproto.repo.uploadBlob#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
hasRequiredScope(scope, 'atproto') |
permissions.assertBlob({ mime: encoding }) |
| Required scope |
atproto |
blob:<mime-type> (e.g., blob:image/*) |
| MIME type awareness |
No |
Yes (validates against Content-Type) |
app.bsky.actor.getPreferences#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
Requires auth only |
permissions.assertRpc({ aud, lxm }) |
| Required scope |
Any valid auth |
rpc:app.bsky.actor.getPreferences |
app.bsky.actor.putPreferences#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
Requires auth only |
permissions.assertRpc({ aud, lxm }) |
| Required scope |
Any valid auth |
rpc:app.bsky.actor.putPreferences |
Scope Parsing#
| Feature |
pds.js |
atproto PDS |
| Scope string splitting |
scope.split(' ') |
ScopesSet class |
| Repo scope parsing |
None |
RepoPermission.fromString() |
| Blob scope parsing |
None |
BlobPermission.fromString() |
| RPC scope parsing |
None |
RpcPermission.fromString() |
| Scope validation |
None (accepts any string) |
Validates syntax, ignores invalid |
| Scope normalization |
None |
Sorts, dedupes, simplifies wildcards |
Permission Checking#
| Feature |
pds.js |
atproto PDS |
| Permission class |
None |
ScopePermissions / ScopePermissionsTransition |
allowsRepo(collection, action) |
N/A |
Yes |
allowsBlob(mime) |
N/A |
Yes (with MIME wildcard matching) |
allowsRpc(aud, lxm) |
N/A |
Yes |
| Transition scope handling |
None |
transition:generic bypasses repo/blob checks |
| Error messages |
Generic |
Specific missing scope in error |
OAuth Flow#
| Feature |
pds.js |
atproto PDS |
scopes_supported in metadata |
['atproto'] |
['atproto'] (but accepts granular) |
| Scope validation at PAR |
None |
Validates syntax |
| Scope stored in token |
Yes |
Yes |
| Scope returned in token response |
Yes |
Yes |
atproto scope required |
Checked at endpoints |
Required at token verification |
Transition Scope Behavior (atproto PDS)#
| Scope |
Effect |
transition:generic |
Bypasses ALL repo permission checks |
transition:generic |
Bypasses ALL blob permission checks |
transition:generic |
Allows all RPC except chat.bsky.* |
transition:chat.bsky |
Allows chat.bsky.* RPC methods |
transition:email |
Allows account:email:read |
pds.js does not recognize any transition scopes.
Summary#
| Category |
pds.js |
atproto PDS |
| Scope parsing |
String contains check |
Full parser per scope type |
| Enforcement granularity |
Binary (has atproto or not) |
Per-collection, per-action |
| Transition scope support |
None |
Full |
| MIME-aware blob scopes |
No |
Yes |
| RPC scopes |
No |
Yes |
| Error specificity |
Generic 403 |
Names missing scope |
Gaps to Address#
- Scope parsing — Need to parse
repo:*:create and blob:image/* syntax
- Permission class — Need
allowsRepo(collection, action) and allowsBlob(mime) methods
- Transition scopes — Need
transition:generic to bypass checks
- Per-endpoint enforcement — Check specific scope at each write endpoint
- MIME matching —
blob:image/* should match image/png, image/jpeg, etc.
- Error messages — Return which scope is missing, not generic error