chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
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.bskyandtransition:emailnot implemented - Scope validation at PAR — Could validate scope syntax during authorization request