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 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#

  1. Scope parsing — Need to parse repo:*:create and blob:image/* syntax
  2. Permission class — Need allowsRepo(collection, action) and allowsBlob(mime) methods
  3. Transition scopes — Need transition:generic to bypass checks
  4. Per-endpoint enforcement — Check specific scope at each write endpoint
  5. MIME matchingblob:image/* should match image/png, image/jpeg, etc.
  6. Error messages — Return which scope is missing, not generic error