~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
146 lines · 6.3 KB Raw History
# 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 scopes** — `rpc:<aud>:<lxm>` parsing and enforcement not implemented
2. **Additional transition scopes** — `transition:chat.bsky` and `transition:email` not implemented
3. **Scope validation at PAR** — Could validate scope syntax during authorization request