# 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::` | Granular | Not parsed | Full parsing + enforcement | | `blob:` | Granular | Not parsed | Full parsing + enforcement | | `rpc::` | 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::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::create` AND `repo::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::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::` 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:` (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 matching** — `blob:image/*` should match `image/png`, `image/jpeg`, etc. 6. **Error messages** — Return which scope is missing, not generic error