+151
docs/scope-comparison.md
+151
docs/scope-comparison.md
···
1
+
# Scope Validation Comparison: pds.js vs atproto PDS
2
+
3
+
Comparison 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
+
127
+
pds.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
+
146
+
1. **Scope parsing** โ Need to parse `repo:*:create` and `blob:image/*` syntax
147
+
2. **Permission class** โ Need `allowsRepo(collection, action)` and `allowsBlob(mime)` methods
148
+
3. **Transition scopes** โ Need `transition:generic` to bypass checks
149
+
4. **Per-endpoint enforcement** โ Check specific scope at each write endpoint
150
+
5. **MIME matching** โ `blob:image/*` should match `image/png`, `image/jpeg`, etc.
151
+
6. **Error messages** โ Return which scope is missing, not generic error