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 |
Full access |
Required for all OAuth |
transition:generic |
Static |
Full access |
Full repo/blob bypass |
transition:email |
Static |
N/A |
Read account email |
transition:chat.bsky |
Static |
N/A |
Chat RPC access |
repo:<collection>?action=<action> |
Granular |
Full parsing + enforcement |
Full parsing + enforcement |
blob:<mime> |
Granular |
Full parsing + enforcement |
Full parsing + enforcement |
rpc:<aud>:<lxm> |
Granular |
Not implemented |
Full parsing + enforcement |
Scope Enforcement by Endpoint#
com.atproto.repo.createRecord#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
ScopePermissions.allowsRepo(collection, 'create') |
permissions.assertRepo({ action: 'create', collection }) |
| Required scope |
repo:<collection>?action=create or transition:generic or atproto |
repo:<collection>?action=create or transition:generic or atproto |
| OAuth-only check |
Yes (legacy tokens without scope bypass) |
Yes (legacy Bearer bypasses) |
| Error response |
403 "Missing required scope "repo:...?action=..."" |
403 "Missing required scope "repo:...?action=..."" |
com.atproto.repo.putRecord#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
allowsRepo(collection, 'create') AND allowsRepo(collection, 'update') |
assertRepo({ action: 'create' }) AND assertRepo({ action: 'update' }) |
| Required scope |
repo:<collection>?action=create&action=update |
repo:<collection>?action=create&action=update |
| Notes |
Requires both since putRecord can create or update |
Requires both since putRecord can create or update |
com.atproto.repo.deleteRecord#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
ScopePermissions.allowsRepo(collection, 'delete') |
permissions.assertRepo({ action: 'delete', collection }) |
| Required scope |
repo:<collection>?action=delete |
repo:<collection>?action=delete |
com.atproto.repo.applyWrites#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
Iterates all writes, checks each unique action/collection pair |
Iterates all writes, asserts each unique action/collection pair |
| Required scope |
All repo:<collection>?action=<action> for each write |
All repo:<collection>?action=<action> for each write |
| Per-write validation |
Yes |
Yes |
com.atproto.repo.uploadBlob#
| Aspect |
pds.js |
atproto PDS |
| Scope check |
ScopePermissions.allowsBlob(contentType) |
permissions.assertBlob({ mime: encoding }) |
| Required scope |
blob:<mime-type> (e.g., blob:image/*) |
blob:<mime-type> (e.g., blob:image/*) |
| MIME type awareness |
Yes (validates against Content-Type) |
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 |
parseRepoScope() |
RepoPermission.fromString() |
| Repo scope format |
repo:collection?action=create&action=update |
repo:collection?action=create&action=update |
| Blob scope parsing |
parseBlobScope() |
BlobPermission.fromString() |
| RPC scope parsing |
None |
RpcPermission.fromString() |
| Scope validation |
Returns null for invalid |
Validates syntax, ignores invalid |
| Action deduplication |
Yes (via Set) |
Yes |
| Default actions |
All (create, update, delete) when no ?action= |
All (create, update, delete) when no ?action= |
Permission Checking#
| Feature |
pds.js |
atproto PDS |
| Permission class |
ScopePermissions |
ScopePermissions / ScopePermissionsTransition |
allowsRepo(collection, action) |
Yes |
Yes |
allowsBlob(mime) |
Yes (with MIME wildcard matching) |
Yes (with MIME wildcard matching) |
allowsRpc(aud, lxm) |
N/A |
Yes |
| Transition scope handling |
transition:generic bypasses repo/blob checks |
transition:generic bypasses repo/blob checks |
| Error messages |
Specific missing scope in error |
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#
| Scope |
pds.js |
atproto PDS |
transition:generic |
Bypasses all repo/blob permission checks |
Bypasses ALL repo/blob permission checks |
transition:chat.bsky |
Not implemented |
Allows chat.bsky.* RPC methods |
transition:email |
Not implemented |
Allows account:email:read |
Summary#
| Category |
pds.js |
atproto PDS |
| Scope parsing |
Full parser for repo/blob |
Full parser per scope type |
| Enforcement granularity |
Per-collection, per-action |
Per-collection, per-action |
| Transition scope support |
transition:generic only |
Full |
| MIME-aware blob scopes |
Yes |
Yes |
| RPC scopes |
No |
Yes |
| Error specificity |
Names missing scope |
Names missing scope |
Remaining Gaps#
- RPC scopes —
rpc:<aud>:<lxm> parsing and enforcement not implemented
- Additional transition scopes —
transition:chat.bsky and transition:email not implemented
- Scope validation at PAR — Could validate scope syntax during authorization request