@atproto/oauth-scopes 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/atproto-oauth-scope.d.ts +17 -0
  3. package/dist/atproto-oauth-scope.d.ts.map +1 -0
  4. package/dist/atproto-oauth-scope.js +67 -0
  5. package/dist/atproto-oauth-scope.js.map +1 -0
  6. package/dist/index.d.ts +9 -13
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +9 -13
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/lexicon.d.ts +2 -0
  11. package/dist/lib/lexicon.d.ts.map +1 -0
  12. package/dist/lib/lexicon.js +3 -0
  13. package/dist/lib/lexicon.js.map +1 -0
  14. package/dist/lib/mime.d.ts +1 -1
  15. package/dist/lib/mime.d.ts.map +1 -1
  16. package/dist/lib/mime.js +2 -0
  17. package/dist/lib/mime.js.map +1 -1
  18. package/dist/lib/nsid.d.ts +2 -2
  19. package/dist/lib/nsid.d.ts.map +1 -1
  20. package/dist/lib/nsid.js +4 -6
  21. package/dist/lib/nsid.js.map +1 -1
  22. package/dist/lib/parser.d.ts +29 -0
  23. package/dist/lib/parser.d.ts.map +1 -0
  24. package/dist/lib/parser.js +152 -0
  25. package/dist/lib/parser.js.map +1 -0
  26. package/dist/lib/resource-permission.d.ts +10 -0
  27. package/dist/lib/resource-permission.d.ts.map +1 -0
  28. package/dist/lib/resource-permission.js +3 -0
  29. package/dist/lib/resource-permission.js.map +1 -0
  30. package/dist/lib/syntax-lexicon.d.ts +26 -0
  31. package/dist/lib/syntax-lexicon.d.ts.map +1 -0
  32. package/dist/lib/syntax-lexicon.js +58 -0
  33. package/dist/lib/syntax-lexicon.js.map +1 -0
  34. package/dist/lib/syntax-string.d.ts +16 -0
  35. package/dist/lib/syntax-string.d.ts.map +1 -0
  36. package/dist/lib/syntax-string.js +121 -0
  37. package/dist/lib/syntax-string.js.map +1 -0
  38. package/dist/lib/syntax.d.ts +23 -0
  39. package/dist/lib/syntax.d.ts.map +1 -0
  40. package/dist/lib/syntax.js +22 -0
  41. package/dist/lib/syntax.js.map +1 -0
  42. package/dist/lib/util.d.ts +5 -1
  43. package/dist/lib/util.d.ts.map +1 -1
  44. package/dist/lib/util.js +8 -12
  45. package/dist/lib/util.js.map +1 -1
  46. package/dist/scope-permissions-transition.d.ts +15 -0
  47. package/dist/scope-permissions-transition.d.ts.map +1 -0
  48. package/dist/{permission-set-transition.js → scope-permissions-transition.js} +5 -5
  49. package/dist/scope-permissions-transition.js.map +1 -0
  50. package/dist/scope-permissions.d.ts +22 -0
  51. package/dist/scope-permissions.d.ts.map +1 -0
  52. package/dist/{permission-set.js → scope-permissions.js} +20 -16
  53. package/dist/scope-permissions.js.map +1 -0
  54. package/dist/scopes/account-permission.d.ts +35 -0
  55. package/dist/scopes/account-permission.d.ts.map +1 -0
  56. package/dist/scopes/account-permission.js +71 -0
  57. package/dist/scopes/account-permission.js.map +1 -0
  58. package/dist/scopes/blob-permission.d.ts +27 -0
  59. package/dist/scopes/blob-permission.d.ts.map +1 -0
  60. package/dist/scopes/blob-permission.js +86 -0
  61. package/dist/scopes/blob-permission.js.map +1 -0
  62. package/dist/scopes/identity-permission.d.ts +25 -0
  63. package/dist/scopes/identity-permission.d.ts.map +1 -0
  64. package/dist/scopes/identity-permission.js +53 -0
  65. package/dist/scopes/identity-permission.js.map +1 -0
  66. package/dist/scopes/include-scope.d.ts +54 -0
  67. package/dist/scopes/include-scope.d.ts.map +1 -0
  68. package/dist/scopes/include-scope.js +156 -0
  69. package/dist/scopes/include-scope.js.map +1 -0
  70. package/dist/scopes/repo-permission.d.ts +40 -0
  71. package/dist/scopes/repo-permission.d.ts.map +1 -0
  72. package/dist/scopes/repo-permission.js +101 -0
  73. package/dist/scopes/repo-permission.js.map +1 -0
  74. package/dist/scopes/rpc-permission.d.ts +38 -0
  75. package/dist/scopes/rpc-permission.d.ts.map +1 -0
  76. package/dist/scopes/rpc-permission.js +81 -0
  77. package/dist/scopes/rpc-permission.js.map +1 -0
  78. package/dist/scopes-set.d.ts +12 -1
  79. package/dist/scopes-set.d.ts.map +1 -1
  80. package/dist/scopes-set.js +49 -3
  81. package/dist/scopes-set.js.map +1 -1
  82. package/package.json +7 -3
  83. package/src/atproto-oauth-scope.ts +79 -0
  84. package/src/index.ts +10 -14
  85. package/src/lib/lexicon.ts +1 -0
  86. package/src/lib/mime.ts +2 -1
  87. package/src/lib/nsid.ts +5 -6
  88. package/src/lib/parser.ts +176 -0
  89. package/src/lib/resource-permission.ts +10 -0
  90. package/src/lib/syntax-lexicon.ts +55 -0
  91. package/src/lib/syntax-string.test.ts +130 -0
  92. package/src/lib/syntax-string.ts +132 -0
  93. package/src/lib/syntax.test.ts +43 -0
  94. package/src/lib/syntax.ts +47 -0
  95. package/src/lib/util.ts +11 -12
  96. package/src/{permission-set-transition.test.ts → scope-permissions-transition.test.ts} +33 -20
  97. package/src/{permission-set-transition.ts → scope-permissions-transition.ts} +11 -11
  98. package/src/{permission-set.test.ts → scope-permissions.test.ts} +77 -35
  99. package/src/scope-permissions.ts +91 -0
  100. package/src/{resources/account-scope.test.ts → scopes/account-permission.test.ts} +45 -33
  101. package/src/scopes/account-permission.ts +75 -0
  102. package/src/{resources/blob-scope.test.ts → scopes/blob-permission.test.ts} +31 -23
  103. package/src/scopes/blob-permission.ts +105 -0
  104. package/src/{resources/identity-scope.test.ts → scopes/identity-permission.test.ts} +13 -13
  105. package/src/scopes/identity-permission.ts +54 -0
  106. package/src/scopes/include-scope.test.ts +626 -0
  107. package/src/scopes/include-scope.ts +168 -0
  108. package/src/{resources/repo-scope.test.ts → scopes/repo-permission.test.ts} +77 -65
  109. package/src/scopes/repo-permission.ts +111 -0
  110. package/src/scopes/rpc-permission.test.ts +323 -0
  111. package/src/scopes/rpc-permission.ts +85 -0
  112. package/src/scopes-set.test.ts +5 -5
  113. package/src/scopes-set.ts +79 -5
  114. package/tsconfig.build.tsbuildinfo +1 -1
  115. package/tsconfig.tests.tsbuildinfo +1 -1
  116. package/dist/lib/did.d.ts +0 -3
  117. package/dist/lib/did.d.ts.map +0 -1
  118. package/dist/lib/did.js +0 -6
  119. package/dist/lib/did.js.map +0 -1
  120. package/dist/parser.d.ts +0 -31
  121. package/dist/parser.d.ts.map +0 -1
  122. package/dist/parser.js +0 -118
  123. package/dist/parser.js.map +0 -1
  124. package/dist/permission-set-transition.d.ts +0 -15
  125. package/dist/permission-set-transition.d.ts.map +0 -1
  126. package/dist/permission-set-transition.js.map +0 -1
  127. package/dist/permission-set.d.ts +0 -22
  128. package/dist/permission-set.d.ts.map +0 -1
  129. package/dist/permission-set.js.map +0 -1
  130. package/dist/resources/account-scope.d.ts +0 -35
  131. package/dist/resources/account-scope.d.ts.map +0 -1
  132. package/dist/resources/account-scope.js +0 -60
  133. package/dist/resources/account-scope.js.map +0 -1
  134. package/dist/resources/blob-scope.d.ts +0 -25
  135. package/dist/resources/blob-scope.d.ts.map +0 -1
  136. package/dist/resources/blob-scope.js +0 -74
  137. package/dist/resources/blob-scope.js.map +0 -1
  138. package/dist/resources/identity-scope.d.ts +0 -25
  139. package/dist/resources/identity-scope.d.ts.map +0 -1
  140. package/dist/resources/identity-scope.js +0 -46
  141. package/dist/resources/identity-scope.js.map +0 -1
  142. package/dist/resources/repo-scope.d.ts +0 -37
  143. package/dist/resources/repo-scope.d.ts.map +0 -1
  144. package/dist/resources/repo-scope.js +0 -92
  145. package/dist/resources/repo-scope.js.map +0 -1
  146. package/dist/resources/rpc-scope.d.ts +0 -31
  147. package/dist/resources/rpc-scope.d.ts.map +0 -1
  148. package/dist/resources/rpc-scope.js +0 -74
  149. package/dist/resources/rpc-scope.js.map +0 -1
  150. package/dist/syntax.d.ts +0 -76
  151. package/dist/syntax.d.ts.map +0 -1
  152. package/dist/syntax.js +0 -249
  153. package/dist/syntax.js.map +0 -1
  154. package/dist/utilities.d.ts +0 -17
  155. package/dist/utilities.d.ts.map +0 -1
  156. package/dist/utilities.js +0 -108
  157. package/dist/utilities.js.map +0 -1
  158. package/src/lib/did.ts +0 -3
  159. package/src/parser.ts +0 -150
  160. package/src/permission-set.ts +0 -78
  161. package/src/resources/account-scope.ts +0 -66
  162. package/src/resources/blob-scope.ts +0 -86
  163. package/src/resources/identity-scope.ts +0 -49
  164. package/src/resources/repo-scope.ts +0 -101
  165. package/src/resources/rpc-scope.test.ts +0 -280
  166. package/src/resources/rpc-scope.ts +0 -77
  167. package/src/syntax.test.ts +0 -203
  168. package/src/syntax.ts +0 -325
  169. package/src/utilities.ts +0 -109
@@ -1,9 +1,9 @@
1
- import { PermissionSet } from './permission-set.js'
1
+ import { ScopePermissions } from './scope-permissions.js'
2
2
 
3
- describe('PermissionSet', () => {
3
+ describe('ScopePermissions', () => {
4
4
  describe('allowsAccount', () => {
5
5
  it('should properly allow "account:email"', () => {
6
- const set = new PermissionSet('account:email')
6
+ const set = new ScopePermissions('account:email')
7
7
 
8
8
  expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(true)
9
9
  expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
@@ -18,7 +18,7 @@ describe('PermissionSet', () => {
18
18
  })
19
19
 
20
20
  it('should ignore "transition:email"', () => {
21
- const set = new PermissionSet('transition:email')
21
+ const set = new ScopePermissions('transition:email')
22
22
 
23
23
  expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(false)
24
24
  expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
@@ -27,31 +27,31 @@ describe('PermissionSet', () => {
27
27
 
28
28
  describe('allowsBlob', () => {
29
29
  it('should allow any mime with "blob:*/*"', () => {
30
- const set = new PermissionSet('blob:*/*')
30
+ const set = new ScopePermissions('blob:*/*')
31
31
  expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)
32
32
  expect(set.allowsBlob({ mime: 'application/json' })).toBe(true)
33
33
  })
34
34
 
35
35
  it('should only allow images with "blob:image/*"', () => {
36
- const set = new PermissionSet('blob:image/*')
36
+ const set = new ScopePermissions('blob:image/*')
37
37
  expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)
38
38
  expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
39
39
  })
40
40
 
41
41
  it('should ignore invalid scope "blob:*"', () => {
42
- const set = new PermissionSet('blob:*')
42
+ const set = new ScopePermissions('blob:*')
43
43
  expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
44
44
  expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
45
45
  })
46
46
 
47
47
  it('should ignore invalid scope "blob:/image"', () => {
48
- const set = new PermissionSet('blob:/image')
48
+ const set = new ScopePermissions('blob:/image')
49
49
  expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
50
50
  expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
51
51
  })
52
52
 
53
53
  it('should ignore "transition:generic"', () => {
54
- const set = new PermissionSet('transition:generic')
54
+ const set = new ScopePermissions('transition:generic')
55
55
  expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
56
56
  expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
57
57
  })
@@ -59,7 +59,7 @@ describe('PermissionSet', () => {
59
59
 
60
60
  describe('allowsRepo', () => {
61
61
  it('should allow any repo action with "repo:*"', () => {
62
- const set = new PermissionSet('repo:*')
62
+ const set = new ScopePermissions('repo:*')
63
63
  expect(
64
64
  set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
65
65
  ).toBe(true)
@@ -72,7 +72,7 @@ describe('PermissionSet', () => {
72
72
  })
73
73
 
74
74
  it('should allow specific repo actions', () => {
75
- const set = new PermissionSet('repo:*?action=create')
75
+ const set = new ScopePermissions('repo:*?action=create')
76
76
  expect(
77
77
  set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
78
78
  ).toBe(true)
@@ -91,7 +91,7 @@ describe('PermissionSet', () => {
91
91
  })
92
92
 
93
93
  it('should allow specific repo collection & actions', () => {
94
- const set = new PermissionSet('repo:com.example.foo?action=create')
94
+ const set = new ScopePermissions('repo:com.example.foo?action=create')
95
95
  expect(
96
96
  set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
97
97
  ).toBe(true)
@@ -110,7 +110,7 @@ describe('PermissionSet', () => {
110
110
  })
111
111
 
112
112
  it('should ignore transition:generic', () => {
113
- const set = new PermissionSet('transition:generic')
113
+ const set = new ScopePermissions('transition:generic')
114
114
  expect(
115
115
  set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
116
116
  ).toBe(false)
@@ -128,19 +128,28 @@ describe('PermissionSet', () => {
128
128
 
129
129
  describe('allowsRpc', () => {
130
130
  it('should ignore "rpc:*?lxm=*"', () => {
131
- const set = new PermissionSet('rpc:*?lxm=*')
131
+ const set = new ScopePermissions('rpc:*?lxm=*')
132
132
  expect(
133
- set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.method' }),
133
+ set.allowsRpc({
134
+ aud: 'did:web:example.com',
135
+ lxm: 'com.example.method',
136
+ }),
134
137
  ).toBe(false)
135
138
  expect(
136
- set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.getFeed' }),
139
+ set.allowsRpc({
140
+ aud: 'did:web:example.com',
141
+ lxm: 'app.bsky.feed.getFeed',
142
+ }),
137
143
  ).toBe(false)
138
144
  })
139
145
 
140
146
  it('should allow constraining "lxm"', () => {
141
- const set = new PermissionSet('rpc:app.bsky.feed.getFeed?aud=*')
147
+ const set = new ScopePermissions('rpc:app.bsky.feed.getFeed?aud=*')
142
148
  expect(
143
- set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.getFeed' }),
149
+ set.allowsRpc({
150
+ aud: 'did:web:example.com',
151
+ lxm: 'app.bsky.feed.getFeed',
152
+ }),
144
153
  ).toBe(true)
145
154
  expect(
146
155
  set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),
@@ -149,38 +158,62 @@ describe('PermissionSet', () => {
149
158
  // Control
150
159
 
151
160
  expect(
152
- set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.method' }),
161
+ set.allowsRpc({
162
+ aud: 'did:web:example.com',
163
+ lxm: 'com.example.method',
164
+ }),
153
165
  ).toBe(false)
154
166
  })
155
167
 
156
168
  it('should allow constraining "aud"', () => {
157
- const set = new PermissionSet('rpc:*?aud=did:example:123')
169
+ const set = new ScopePermissions('rpc:*?aud=did:web:example.com%23foo')
158
170
  expect(
159
- set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.method' }),
171
+ set.allowsRpc({
172
+ aud: 'did:web:example.com#foo',
173
+ lxm: 'com.example.method',
174
+ }),
160
175
  ).toBe(true)
161
176
  expect(
162
- set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.getFeed' }),
177
+ set.allowsRpc({
178
+ aud: 'did:web:example.com#foo',
179
+ lxm: 'app.bsky.feed.getFeed',
180
+ }),
163
181
  ).toBe(true)
164
182
 
165
183
  // Control
166
184
 
167
185
  expect(
168
- set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'com.example.method' }),
186
+ set.allowsRpc({
187
+ aud: 'did:web:bar.com#foo', // invalid aud (wrong service id)
188
+ lxm: 'com.example.method',
189
+ }),
190
+ ).toBe(false)
191
+ expect(
192
+ set.allowsRpc({
193
+ aud: 'did:web:example.com', // invalid aud (no service id)
194
+ lxm: 'com.example.method',
195
+ }),
169
196
  ).toBe(false)
170
197
  })
171
198
 
172
199
  it('should allow constraining "lxm" and "aud"', () => {
173
- const set = new PermissionSet(
174
- 'rpc:app.bsky.feed.getFeed?aud=did:example:123',
200
+ const set = new ScopePermissions(
201
+ 'rpc:app.bsky.feed.getFeed?aud=did:web:example.com%23foo',
175
202
  )
176
203
  expect(
177
- set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.getFeed' }),
204
+ set.allowsRpc({
205
+ aud: 'did:web:example.com#foo',
206
+ lxm: 'app.bsky.feed.getFeed',
207
+ }),
178
208
  ).toBe(true)
179
209
 
180
210
  // Control
181
211
 
182
212
  expect(
183
- set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.method' }),
213
+ set.allowsRpc({
214
+ aud: 'did:web:example.com',
215
+ lxm: 'com.example.method',
216
+ }),
184
217
  ).toBe(false)
185
218
  expect(
186
219
  set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),
@@ -188,26 +221,32 @@ describe('PermissionSet', () => {
188
221
  })
189
222
 
190
223
  it('should ignore "transition:generic"', () => {
191
- const set = new PermissionSet('transition:generic')
224
+ const set = new ScopePermissions('transition:generic')
192
225
  expect(
193
- set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.getFeed' }),
226
+ set.allowsRpc({
227
+ aud: 'did:web:example.com',
228
+ lxm: 'app.bsky.feed.getFeed',
229
+ }),
194
230
  ).toBe(false)
195
231
  expect(
196
- set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.method' }),
232
+ set.allowsRpc({
233
+ aud: 'did:web:example.com',
234
+ lxm: 'com.example.method',
235
+ }),
197
236
  ).toBe(false)
198
237
  })
199
238
 
200
239
  it('should ignore "transition:chat.bsky"', () => {
201
- const set = new PermissionSet('transition:chat.bsky')
240
+ const set = new ScopePermissions('transition:chat.bsky')
202
241
  expect(
203
242
  set.allowsRpc({
204
- aud: 'did:example:123',
243
+ aud: 'did:web:example.com',
205
244
  lxm: 'chat.bsky.message.send',
206
245
  }),
207
246
  ).toBe(false)
208
247
  expect(
209
248
  set.allowsRpc({
210
- aud: 'did:example:123',
249
+ aud: 'did:web:example.com',
211
250
  lxm: 'chat.bsky.conversation.get',
212
251
  }),
213
252
  ).toBe(false)
@@ -215,10 +254,13 @@ describe('PermissionSet', () => {
215
254
  // Control
216
255
 
217
256
  expect(
218
- set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.post' }),
257
+ set.allowsRpc({
258
+ aud: 'did:web:example.com',
259
+ lxm: 'app.bsky.feed.post',
260
+ }),
219
261
  ).toBe(false)
220
262
  expect(
221
- set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.foo' }),
263
+ set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
222
264
  ).toBe(false)
223
265
  })
224
266
  })
@@ -0,0 +1,91 @@
1
+ import { ScopeMissingError } from './scope-missing-error.js'
2
+ import {
3
+ AccountPermission,
4
+ AccountPermissionMatch,
5
+ } from './scopes/account-permission.js'
6
+ import {
7
+ BlobPermission,
8
+ BlobPermissionMatch,
9
+ } from './scopes/blob-permission.js'
10
+ import {
11
+ IdentityPermission,
12
+ IdentityPermissionMatch,
13
+ } from './scopes/identity-permission.js'
14
+ import {
15
+ RepoPermission,
16
+ RepoPermissionMatch,
17
+ } from './scopes/repo-permission.js'
18
+ import { RpcPermission, RpcPermissionMatch } from './scopes/rpc-permission.js'
19
+ import { ScopesSet } from './scopes-set.js'
20
+
21
+ export type {
22
+ AccountPermissionMatch,
23
+ BlobPermissionMatch,
24
+ IdentityPermissionMatch,
25
+ RepoPermissionMatch,
26
+ RpcPermissionMatch,
27
+ }
28
+
29
+ export class ScopePermissions {
30
+ public readonly scopes: ScopesSet
31
+
32
+ constructor(scope?: null | string | Iterable<string>) {
33
+ this.scopes = new ScopesSet(
34
+ !scope // "" | null | undefined
35
+ ? undefined
36
+ : typeof scope === 'string'
37
+ ? scope.split(' ')
38
+ : scope,
39
+ )
40
+ }
41
+
42
+ public allowsAccount(options: AccountPermissionMatch): boolean {
43
+ return this.scopes.matches('account', options)
44
+ }
45
+ public assertAccount(options: AccountPermissionMatch): void {
46
+ if (!this.allowsAccount(options)) {
47
+ const scope = AccountPermission.scopeNeededFor(options)
48
+ throw new ScopeMissingError(scope)
49
+ }
50
+ }
51
+
52
+ public allowsIdentity(options: IdentityPermissionMatch): boolean {
53
+ return this.scopes.matches('identity', options)
54
+ }
55
+ public assertIdentity(options: IdentityPermissionMatch): void {
56
+ if (!this.allowsIdentity(options)) {
57
+ const scope = IdentityPermission.scopeNeededFor(options)
58
+ throw new ScopeMissingError(scope)
59
+ }
60
+ }
61
+
62
+ public allowsBlob(options: BlobPermissionMatch): boolean {
63
+ return this.scopes.matches('blob', options)
64
+ }
65
+ public assertBlob(options: BlobPermissionMatch): void {
66
+ if (!this.allowsBlob(options)) {
67
+ const scope = BlobPermission.scopeNeededFor(options)
68
+ throw new ScopeMissingError(scope)
69
+ }
70
+ }
71
+
72
+ public allowsRepo(options: RepoPermissionMatch): boolean {
73
+ return this.scopes.matches('repo', options)
74
+ }
75
+ public assertRepo(options: RepoPermissionMatch): void {
76
+ if (!this.allowsRepo(options)) {
77
+ const scope = RepoPermission.scopeNeededFor(options)
78
+ throw new ScopeMissingError(scope)
79
+ }
80
+ }
81
+
82
+ public allowsRpc(options: RpcPermissionMatch): boolean {
83
+ return this.scopes.matches('rpc', options)
84
+ }
85
+ public assertRpc(options: RpcPermissionMatch): void {
86
+ if (!this.allowsRpc(options)) {
87
+ const scope = RpcPermission.scopeNeededFor(options)
88
+ throw new ScopeMissingError(scope)
89
+ }
90
+ }
91
+ }
@@ -1,67 +1,71 @@
1
- import { AccountScope } from './account-scope.js'
1
+ import { AccountPermission } from './account-permission.js'
2
2
 
3
- describe('AccountScope', () => {
3
+ describe('AccountPermission', () => {
4
4
  describe('static', () => {
5
5
  describe('fromString', () => {
6
6
  it('should parse valid scope strings', () => {
7
- const scope1 = AccountScope.fromString('account:email?action=read')
7
+ const scope1 = AccountPermission.fromString('account:email?action=read')
8
8
  expect(scope1).not.toBeNull()
9
9
  expect(scope1!.attr).toBe('email')
10
10
  expect(scope1!.action).toBe('read')
11
11
 
12
- const scope2 = AccountScope.fromString('account:repo?action=manage')
12
+ const scope2 = AccountPermission.fromString(
13
+ 'account:repo?action=manage',
14
+ )
13
15
  expect(scope2).not.toBeNull()
14
16
  expect(scope2!.attr).toBe('repo')
15
17
  expect(scope2!.action).toBe('manage')
16
18
  })
17
19
 
18
20
  it('should parse scope without action (defaults to read)', () => {
19
- const scope = AccountScope.fromString('account:status')
21
+ const scope = AccountPermission.fromString('account:status')
20
22
  expect(scope).not.toBeNull()
21
23
  expect(scope!.attr).toBe('status')
22
24
  expect(scope!.action).toBe('read')
23
25
  })
24
26
 
25
27
  it('should reject invalid attribute names', () => {
26
- const scope = AccountScope.fromString('account:invalid')
28
+ const scope = AccountPermission.fromString('account:invalid')
27
29
  expect(scope).toBeNull()
28
30
  })
29
31
 
30
32
  it('should reject invalid action names', () => {
31
- const scope = AccountScope.fromString('account:email?action=invalid')
33
+ const scope = AccountPermission.fromString(
34
+ 'account:email?action=invalid',
35
+ )
32
36
  expect(scope).toBeNull()
33
37
  })
34
38
 
35
39
  it('should reject malformed scope strings', () => {
36
- expect(AccountScope.fromString('invalid:email')).toBeNull()
37
- expect(AccountScope.fromString('account')).toBeNull()
38
- expect(AccountScope.fromString('')).toBeNull()
39
- expect(AccountScope.fromString('account:')).toBeNull()
40
+ expect(AccountPermission.fromString('invalid:email')).toBeNull()
41
+ expect(AccountPermission.fromString('account')).toBeNull()
42
+ expect(AccountPermission.fromString('')).toBeNull()
43
+ expect(AccountPermission.fromString('account:')).toBeNull()
40
44
  })
41
45
  })
42
46
 
43
47
  describe('scopeNeededFor', () => {
44
48
  it('should return correct scope string for read actions', () => {
45
49
  expect(
46
- AccountScope.scopeNeededFor({ attr: 'email', action: 'read' }),
50
+ AccountPermission.scopeNeededFor({ attr: 'email', action: 'read' }),
47
51
  ).toBe('account:email')
48
52
  expect(
49
- AccountScope.scopeNeededFor({ attr: 'repo', action: 'read' }),
53
+ AccountPermission.scopeNeededFor({ attr: 'repo', action: 'read' }),
50
54
  ).toBe('account:repo')
51
55
  expect(
52
- AccountScope.scopeNeededFor({ attr: 'status', action: 'read' }),
56
+ AccountPermission.scopeNeededFor({ attr: 'status', action: 'read' }),
53
57
  ).toBe('account:status')
54
58
  })
55
59
 
56
60
  it('should return correct scope string for manage actions', () => {
57
61
  expect(
58
- AccountScope.scopeNeededFor({ attr: 'email', action: 'manage' }),
62
+ AccountPermission.scopeNeededFor({ attr: 'email', action: 'manage' }),
59
63
  ).toBe('account:email?action=manage')
60
64
  expect(
61
- AccountScope.scopeNeededFor({ attr: 'repo', action: 'manage' }),
65
+ AccountPermission.scopeNeededFor({ attr: 'repo', action: 'manage' }),
62
66
  ).toBe('account:repo?action=manage')
63
67
  expect(
64
- AccountScope.scopeNeededFor({
68
+ AccountPermission.scopeNeededFor({
65
69
  attr: 'status',
66
70
  action: 'manage',
67
71
  }),
@@ -73,40 +77,44 @@ describe('AccountScope', () => {
73
77
  describe('instance', () => {
74
78
  describe('matches', () => {
75
79
  it('should match read action', () => {
76
- const scope = AccountScope.fromString('account:email?action=read')
80
+ const scope = AccountPermission.fromString('account:email?action=read')
77
81
  expect(scope).not.toBeNull()
78
82
  expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
79
83
  })
80
84
 
81
85
  it('should match manage action', () => {
82
- const scope = AccountScope.fromString('account:repo?action=manage')
86
+ const scope = AccountPermission.fromString('account:repo?action=manage')
83
87
  expect(scope).not.toBeNull()
84
88
  expect(scope!.matches({ attr: 'repo', action: 'manage' })).toBe(true)
85
89
  })
86
90
 
87
91
  it('should not match unspecified action', () => {
88
- const scope = AccountScope.fromString('account:email?action=read')
92
+ const scope = AccountPermission.fromString('account:email?action=read')
89
93
  expect(scope).not.toBeNull()
90
94
  expect(scope!.matches({ attr: 'email', action: 'manage' })).toBe(false)
91
95
  })
92
96
 
93
97
  it('should not match different attribute', () => {
94
- const scope = AccountScope.fromString('account:email?action=read')
98
+ const scope = AccountPermission.fromString('account:email?action=read')
95
99
  expect(scope).not.toBeNull()
96
100
  expect(scope!.matches({ attr: 'repo', action: 'read' })).toBe(false)
97
101
  })
98
102
 
99
103
  it('should default to "read" action', () => {
100
- const scope = AccountScope.fromString('account:email')
104
+ const scope = AccountPermission.fromString('account:email')
101
105
  expect(scope).not.toBeNull()
102
106
  expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
103
107
  expect(scope!.matches({ attr: 'email', action: 'manage' })).toBe(false)
104
108
  })
105
109
 
106
110
  it('should work with all valid attributes', () => {
107
- const emailScope = AccountScope.fromString('account:email?action=read')
108
- const repoScope = AccountScope.fromString('account:repo?action=manage')
109
- const statusScope = AccountScope.fromString(
111
+ const emailScope = AccountPermission.fromString(
112
+ 'account:email?action=read',
113
+ )
114
+ const repoScope = AccountPermission.fromString(
115
+ 'account:repo?action=manage',
116
+ )
117
+ const statusScope = AccountPermission.fromString(
110
118
  'account:status?action=read',
111
119
  )
112
120
 
@@ -126,7 +134,9 @@ describe('AccountScope', () => {
126
134
  })
127
135
 
128
136
  it('should allow read when "manage" action is specified', () => {
129
- const scope = AccountScope.fromString('account:email?action=manage')
137
+ const scope = AccountPermission.fromString(
138
+ 'account:email?action=manage',
139
+ )
130
140
  expect(scope).not.toBeNull()
131
141
  expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
132
142
  })
@@ -134,24 +144,26 @@ describe('AccountScope', () => {
134
144
 
135
145
  describe('toString', () => {
136
146
  it('should format scope with explicit action', () => {
137
- const scope = new AccountScope('email', 'manage')
147
+ const scope = new AccountPermission('email', 'manage')
138
148
  expect(scope.toString()).toBe('account:email?action=manage')
139
149
  })
140
150
 
141
151
  it('should format scope with default action', () => {
142
- const scope = new AccountScope('repo', 'read')
152
+ const scope = new AccountPermission('repo', 'read')
143
153
  expect(scope.toString()).toBe('account:repo')
144
154
  })
145
155
 
146
156
  it('should format all attributes correctly', () => {
147
- expect(new AccountScope('email', 'read').toString()).toBe(
157
+ expect(new AccountPermission('email', 'read').toString()).toBe(
148
158
  'account:email',
149
159
  )
150
- expect(new AccountScope('repo', 'read').toString()).toBe('account:repo')
151
- expect(new AccountScope('status', 'read').toString()).toBe(
160
+ expect(new AccountPermission('repo', 'read').toString()).toBe(
161
+ 'account:repo',
162
+ )
163
+ expect(new AccountPermission('status', 'read').toString()).toBe(
152
164
  'account:status',
153
165
  )
154
- expect(new AccountScope('email', 'manage').toString()).toBe(
166
+ expect(new AccountPermission('email', 'manage').toString()).toBe(
155
167
  'account:email?action=manage',
156
168
  )
157
169
  })
@@ -169,7 +181,7 @@ describe('AccountScope', () => {
169
181
  ]
170
182
 
171
183
  for (const scope of testCases) {
172
- expect(AccountScope.fromString(scope)?.toString()).toBe(scope)
184
+ expect(AccountPermission.fromString(scope)?.toString()).toBe(scope)
173
185
  }
174
186
  })
175
187
  })
@@ -0,0 +1,75 @@
1
+ import { Parser } from '../lib/parser.js'
2
+ import { ResourcePermission } from '../lib/resource-permission.js'
3
+ import { ScopeStringSyntax } from '../lib/syntax-string.js'
4
+ import { ScopeSyntax, isScopeStringFor } from '../lib/syntax.js'
5
+ import { knownValuesValidator } from '../lib/util.js'
6
+
7
+ export const ACCOUNT_ATTRIBUTES = Object.freeze([
8
+ 'email',
9
+ 'repo',
10
+ 'status',
11
+ ] as const)
12
+ export type AccountAttribute = (typeof ACCOUNT_ATTRIBUTES)[number]
13
+
14
+ export const ACCOUNT_ACTIONS = Object.freeze(['read', 'manage'] as const)
15
+ export type AccountAction = (typeof ACCOUNT_ACTIONS)[number]
16
+
17
+ export type AccountPermissionMatch = {
18
+ attr: AccountAttribute
19
+ action: AccountAction
20
+ }
21
+
22
+ export class AccountPermission
23
+ implements ResourcePermission<'account', AccountPermissionMatch>
24
+ {
25
+ constructor(
26
+ public readonly attr: AccountAttribute,
27
+ public readonly action: AccountAction,
28
+ ) {}
29
+
30
+ matches(options: AccountPermissionMatch) {
31
+ return (
32
+ this.attr === options.attr &&
33
+ (this.action === 'manage' || this.action === options.action)
34
+ )
35
+ }
36
+
37
+ toString() {
38
+ return AccountPermission.parser.format(this)
39
+ }
40
+
41
+ protected static readonly parser = new Parser(
42
+ 'account',
43
+ {
44
+ attr: {
45
+ multiple: false,
46
+ required: true,
47
+ validate: knownValuesValidator(ACCOUNT_ATTRIBUTES),
48
+ },
49
+ action: {
50
+ multiple: false,
51
+ required: false,
52
+ validate: knownValuesValidator(ACCOUNT_ACTIONS),
53
+ default: 'read' as const,
54
+ },
55
+ },
56
+ 'attr',
57
+ )
58
+
59
+ static fromString(scope: string) {
60
+ if (!isScopeStringFor(scope, 'account')) return null
61
+ const syntax = ScopeStringSyntax.fromString(scope)
62
+ return AccountPermission.fromSyntax(syntax)
63
+ }
64
+
65
+ static fromSyntax(syntax: ScopeSyntax<'account'>) {
66
+ const result = AccountPermission.parser.parse(syntax)
67
+ if (!result) return null
68
+
69
+ return new AccountPermission(result.attr, result.action)
70
+ }
71
+
72
+ static scopeNeededFor(options: AccountPermissionMatch) {
73
+ return AccountPermission.parser.format(options)
74
+ }
75
+ }