A zero-dependency AT Protocol Personal Data Server written in JavaScript
atproto pds

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#

  1. RPC scopesrpc:<aud>:<lxm> parsing and enforcement not implemented
  2. Additional transition scopestransition:chat.bsky and transition:email not implemented
  3. Scope validation at PAR — Could validate scope syntax during authorization request