this repo has no description
1# Scope Validation Comparison: pds.js vs atproto PDS
2
3Comparison of OAuth scope validation between this implementation and the official AT Protocol PDS.
4
5---
6
7## Scope Types Supported
8
9| Scope Type | Format | pds.js | atproto PDS |
10|------------|--------|--------|-------------|
11| `atproto` | Static | Checked (loose) | Required for all OAuth |
12| `transition:generic` | Static | Not recognized | Full repo/blob bypass |
13| `transition:email` | Static | N/A | Read account email |
14| `transition:chat.bsky` | Static | N/A | Chat RPC access |
15| `repo:<collection>:<action>` | Granular | Not parsed | Full parsing + enforcement |
16| `blob:<mime>` | Granular | Not parsed | Full parsing + enforcement |
17| `rpc:<aud>:<lxm>` | Granular | Not parsed | Full parsing + enforcement |
18
19---
20
21## Scope Enforcement by Endpoint
22
23### com.atproto.repo.createRecord
24
25| Aspect | pds.js | atproto PDS |
26|--------|--------|-------------|
27| Scope check | `hasRequiredScope(scope, 'atproto')` | `permissions.assertRepo({ action: 'create', collection })` |
28| Required scope | `atproto` anywhere in scope string | `repo:<collection>:create` or `transition:generic` or `atproto` |
29| OAuth-only check | No (checks all tokens) | Yes (legacy Bearer bypasses) |
30| Error response | 403 "Insufficient scope for repo write" | 403 "Missing required scope \"repo:...\"" |
31
32### com.atproto.repo.putRecord
33
34| Aspect | pds.js | atproto PDS |
35|--------|--------|-------------|
36| Scope check | `hasRequiredScope(scope, 'atproto')` | `assertRepo({ action: 'create' })` AND `assertRepo({ action: 'update' })` |
37| Required scope | `atproto` | Both `repo:<collection>:create` AND `repo:<collection>:update` |
38| Notes | Single check | Requires both since putRecord can create or update |
39
40### com.atproto.repo.deleteRecord
41
42| Aspect | pds.js | atproto PDS |
43|--------|--------|-------------|
44| Scope check | `hasRequiredScope(scope, 'atproto')` | `permissions.assertRepo({ action: 'delete', collection })` |
45| Required scope | `atproto` | `repo:<collection>:delete` |
46
47### com.atproto.repo.applyWrites
48
49| Aspect | pds.js | atproto PDS |
50|--------|--------|-------------|
51| Scope check | `hasRequiredScope(scope, 'atproto')` | Iterates all writes, asserts each unique action/collection pair |
52| Required scope | `atproto` | All `repo:<collection>:<action>` for each write |
53| Per-write validation | No | Yes |
54
55### com.atproto.repo.uploadBlob
56
57| Aspect | pds.js | atproto PDS |
58|--------|--------|-------------|
59| Scope check | `hasRequiredScope(scope, 'atproto')` | `permissions.assertBlob({ mime: encoding })` |
60| Required scope | `atproto` | `blob:<mime-type>` (e.g., `blob:image/*`) |
61| MIME type awareness | No | Yes (validates against Content-Type) |
62
63### app.bsky.actor.getPreferences
64
65| Aspect | pds.js | atproto PDS |
66|--------|--------|-------------|
67| Scope check | Requires auth only | `permissions.assertRpc({ aud, lxm })` |
68| Required scope | Any valid auth | `rpc:app.bsky.actor.getPreferences` |
69
70### app.bsky.actor.putPreferences
71
72| Aspect | pds.js | atproto PDS |
73|--------|--------|-------------|
74| Scope check | Requires auth only | `permissions.assertRpc({ aud, lxm })` |
75| Required scope | Any valid auth | `rpc:app.bsky.actor.putPreferences` |
76
77---
78
79## Scope Parsing
80
81| Feature | pds.js | atproto PDS |
82|---------|--------|-------------|
83| Scope string splitting | `scope.split(' ')` | `ScopesSet` class |
84| Repo scope parsing | None | `RepoPermission.fromString()` |
85| Blob scope parsing | None | `BlobPermission.fromString()` |
86| RPC scope parsing | None | `RpcPermission.fromString()` |
87| Scope validation | None (accepts any string) | Validates syntax, ignores invalid |
88| Scope normalization | None | Sorts, dedupes, simplifies wildcards |
89
90---
91
92## Permission Checking
93
94| Feature | pds.js | atproto PDS |
95|---------|--------|-------------|
96| Permission class | None | `ScopePermissions` / `ScopePermissionsTransition` |
97| `allowsRepo(collection, action)` | N/A | Yes |
98| `allowsBlob(mime)` | N/A | Yes (with MIME wildcard matching) |
99| `allowsRpc(aud, lxm)` | N/A | Yes |
100| Transition scope handling | None | `transition:generic` bypasses repo/blob checks |
101| Error messages | Generic | Specific missing scope in error |
102
103---
104
105## OAuth Flow
106
107| Feature | pds.js | atproto PDS |
108|---------|--------|-------------|
109| `scopes_supported` in metadata | `['atproto']` | `['atproto']` (but accepts granular) |
110| Scope validation at PAR | None | Validates syntax |
111| Scope stored in token | Yes | Yes |
112| Scope returned in token response | Yes | Yes |
113| `atproto` scope required | Checked at endpoints | Required at token verification |
114
115---
116
117## Transition Scope Behavior (atproto PDS)
118
119| Scope | Effect |
120|-------|--------|
121| `transition:generic` | Bypasses ALL repo permission checks |
122| `transition:generic` | Bypasses ALL blob permission checks |
123| `transition:generic` | Allows all RPC except `chat.bsky.*` |
124| `transition:chat.bsky` | Allows `chat.bsky.*` RPC methods |
125| `transition:email` | Allows `account:email:read` |
126
127pds.js does not recognize any transition scopes.
128
129---
130
131## Summary
132
133| Category | pds.js | atproto PDS |
134|----------|--------|-------------|
135| Scope parsing | String contains check | Full parser per scope type |
136| Enforcement granularity | Binary (has atproto or not) | Per-collection, per-action |
137| Transition scope support | None | Full |
138| MIME-aware blob scopes | No | Yes |
139| RPC scopes | No | Yes |
140| Error specificity | Generic 403 | Names missing scope |
141
142---
143
144## Gaps to Address
145
1461. **Scope parsing** — Need to parse `repo:*:create` and `blob:image/*` syntax
1472. **Permission class** — Need `allowsRepo(collection, action)` and `allowsBlob(mime)` methods
1483. **Transition scopes** — Need `transition:generic` to bypass checks
1494. **Per-endpoint enforcement** — Check specific scope at each write endpoint
1505. **MIME matching** — `blob:image/*` should match `image/png`, `image/jpeg`, etc.
1516. **Error messages** — Return which scope is missing, not generic error