@atproto/oauth-scopes 0.5.1 → 0.5.3

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 (40) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +10 -6
  3. package/jest.config.cjs +0 -14
  4. package/src/atproto-oauth-scope.ts +0 -79
  5. package/src/index.ts +0 -13
  6. package/src/lib/lexicon.ts +0 -21
  7. package/src/lib/mime.test.ts +0 -98
  8. package/src/lib/mime.ts +0 -71
  9. package/src/lib/nsid.ts +0 -5
  10. package/src/lib/parser.ts +0 -176
  11. package/src/lib/resource-permission.ts +0 -10
  12. package/src/lib/syntax-lexicon.ts +0 -55
  13. package/src/lib/syntax-string.test.ts +0 -130
  14. package/src/lib/syntax-string.ts +0 -132
  15. package/src/lib/syntax.test.ts +0 -43
  16. package/src/lib/syntax.ts +0 -54
  17. package/src/lib/util.ts +0 -18
  18. package/src/scope-missing-error.ts +0 -15
  19. package/src/scope-permissions-transition.test.ts +0 -122
  20. package/src/scope-permissions-transition.ts +0 -71
  21. package/src/scope-permissions.test.ts +0 -303
  22. package/src/scope-permissions.ts +0 -91
  23. package/src/scopes/account-permission.test.ts +0 -187
  24. package/src/scopes/account-permission.ts +0 -78
  25. package/src/scopes/blob-permission.test.ts +0 -126
  26. package/src/scopes/blob-permission.ts +0 -105
  27. package/src/scopes/identity-permission.test.ts +0 -80
  28. package/src/scopes/identity-permission.ts +0 -54
  29. package/src/scopes/include-scope.test.ts +0 -637
  30. package/src/scopes/include-scope.ts +0 -208
  31. package/src/scopes/repo-permission.test.ts +0 -267
  32. package/src/scopes/repo-permission.ts +0 -111
  33. package/src/scopes/rpc-permission.test.ts +0 -323
  34. package/src/scopes/rpc-permission.ts +0 -90
  35. package/src/scopes-set.test.ts +0 -47
  36. package/src/scopes-set.ts +0 -134
  37. package/tsconfig.build.json +0 -9
  38. package/tsconfig.build.tsbuildinfo +0 -1
  39. package/tsconfig.json +0 -7
  40. package/tsconfig.tests.json +0 -7
@@ -1,303 +0,0 @@
1
- import { ScopePermissions } from './scope-permissions.js'
2
-
3
- describe('ScopePermissions', () => {
4
- describe('allowsAccount', () => {
5
- it('should properly allow "account:email"', () => {
6
- const set = new ScopePermissions('account:email')
7
-
8
- expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(true)
9
- expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
10
-
11
- expect(set.allowsAccount({ attr: 'repo', action: 'read' })).toBe(false)
12
- expect(set.allowsAccount({ attr: 'repo', action: 'manage' })).toBe(false)
13
-
14
- expect(set.allowsAccount({ attr: 'status', action: 'read' })).toBe(false)
15
- expect(set.allowsAccount({ attr: 'status', action: 'manage' })).toBe(
16
- false,
17
- )
18
- })
19
-
20
- it('should ignore "transition:email"', () => {
21
- const set = new ScopePermissions('transition:email')
22
-
23
- expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(false)
24
- expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
25
- })
26
- })
27
-
28
- describe('allowsBlob', () => {
29
- it('should allow any mime with "blob:*/*"', () => {
30
- const set = new ScopePermissions('blob:*/*')
31
- expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)
32
- expect(set.allowsBlob({ mime: 'application/json' })).toBe(true)
33
- })
34
-
35
- it('should only allow images with "blob:image/*"', () => {
36
- const set = new ScopePermissions('blob:image/*')
37
- expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)
38
- expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
39
- })
40
-
41
- it('should ignore invalid scope "blob:*"', () => {
42
- const set = new ScopePermissions('blob:*')
43
- expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
44
- expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
45
- })
46
-
47
- it('should ignore invalid scope "blob:/image"', () => {
48
- const set = new ScopePermissions('blob:/image')
49
- expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
50
- expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
51
- })
52
-
53
- it('should ignore "transition:generic"', () => {
54
- const set = new ScopePermissions('transition:generic')
55
- expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
56
- expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
57
- })
58
- })
59
-
60
- describe('allowsRepo', () => {
61
- it('should allow any repo action with "repo:*"', () => {
62
- const set = new ScopePermissions('repo:*')
63
- expect(
64
- set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
65
- ).toBe(true)
66
- expect(
67
- set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
68
- ).toBe(true)
69
- expect(
70
- set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
71
- ).toBe(true)
72
- })
73
-
74
- it('should allow specific repo actions', () => {
75
- const set = new ScopePermissions('repo:*?action=create')
76
- expect(
77
- set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
78
- ).toBe(true)
79
- expect(
80
- set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
81
- ).toBe(true)
82
-
83
- // Control
84
-
85
- expect(
86
- set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
87
- ).toBe(false)
88
- expect(
89
- set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
90
- ).toBe(false)
91
- })
92
-
93
- it('should allow specific repo collection & actions', () => {
94
- const set = new ScopePermissions('repo:com.example.foo?action=create')
95
- expect(
96
- set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
97
- ).toBe(true)
98
-
99
- // Control
100
-
101
- expect(
102
- set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
103
- ).toBe(false)
104
- expect(
105
- set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
106
- ).toBe(false)
107
- expect(
108
- set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
109
- ).toBe(false)
110
- })
111
-
112
- it('should ignore transition:generic', () => {
113
- const set = new ScopePermissions('transition:generic')
114
- expect(
115
- set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
116
- ).toBe(false)
117
- expect(
118
- set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
119
- ).toBe(false)
120
- expect(
121
- set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
122
- ).toBe(false)
123
- expect(
124
- set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
125
- ).toBe(false)
126
- })
127
- })
128
-
129
- describe('allowsRpc', () => {
130
- it('should ignore "rpc:*?lxm=*"', () => {
131
- const set = new ScopePermissions('rpc:*?lxm=*')
132
- expect(
133
- set.allowsRpc({
134
- aud: 'did:web:example.com',
135
- lxm: 'com.example.method',
136
- }),
137
- ).toBe(false)
138
- expect(
139
- set.allowsRpc({
140
- aud: 'did:web:example.com',
141
- lxm: 'app.bsky.feed.getFeed',
142
- }),
143
- ).toBe(false)
144
- })
145
-
146
- it('should allow constraining "lxm"', () => {
147
- const set = new ScopePermissions('rpc:app.bsky.feed.getFeed?aud=*')
148
- expect(
149
- set.allowsRpc({
150
- aud: 'did:web:example.com',
151
- lxm: 'app.bsky.feed.getFeed',
152
- }),
153
- ).toBe(true)
154
- expect(
155
- set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),
156
- ).toBe(true)
157
-
158
- // Control
159
-
160
- expect(
161
- set.allowsRpc({
162
- aud: 'did:web:example.com',
163
- lxm: 'com.example.method',
164
- }),
165
- ).toBe(false)
166
- })
167
-
168
- it('should allow constraining "aud"', () => {
169
- const set = new ScopePermissions('rpc:*?aud=did:web:example.com%23foo')
170
- expect(
171
- set.allowsRpc({
172
- aud: 'did:web:example.com#foo',
173
- lxm: 'com.example.method',
174
- }),
175
- ).toBe(true)
176
- expect(
177
- set.allowsRpc({
178
- aud: 'did:web:example.com#foo',
179
- lxm: 'app.bsky.feed.getFeed',
180
- }),
181
- ).toBe(true)
182
-
183
- // Control
184
-
185
- expect(
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
- }),
196
- ).toBe(false)
197
- })
198
-
199
- it('should allow constraining "lxm" and "aud"', () => {
200
- const set = new ScopePermissions(
201
- 'rpc:app.bsky.feed.getFeed?aud=did:web:example.com%23foo',
202
- )
203
- expect(
204
- set.allowsRpc({
205
- aud: 'did:web:example.com#foo',
206
- lxm: 'app.bsky.feed.getFeed',
207
- }),
208
- ).toBe(true)
209
-
210
- // Control
211
-
212
- expect(
213
- set.allowsRpc({
214
- aud: 'did:web:example.com',
215
- lxm: 'com.example.method',
216
- }),
217
- ).toBe(false)
218
- expect(
219
- set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),
220
- ).toBe(false)
221
- })
222
-
223
- it('should ignore "transition:generic"', () => {
224
- const set = new ScopePermissions('transition:generic')
225
- expect(
226
- set.allowsRpc({
227
- aud: 'did:web:example.com',
228
- lxm: 'app.bsky.feed.getFeed',
229
- }),
230
- ).toBe(false)
231
- expect(
232
- set.allowsRpc({
233
- aud: 'did:web:example.com',
234
- lxm: 'com.example.method',
235
- }),
236
- ).toBe(false)
237
- })
238
-
239
- it('should ignore "transition:chat.bsky"', () => {
240
- const set = new ScopePermissions('transition:chat.bsky')
241
- expect(
242
- set.allowsRpc({
243
- aud: 'did:web:example.com',
244
- lxm: 'chat.bsky.message.send',
245
- }),
246
- ).toBe(false)
247
- expect(
248
- set.allowsRpc({
249
- aud: 'did:web:example.com',
250
- lxm: 'chat.bsky.conversation.get',
251
- }),
252
- ).toBe(false)
253
-
254
- // Control
255
-
256
- expect(
257
- set.allowsRpc({
258
- aud: 'did:web:example.com',
259
- lxm: 'app.bsky.feed.post',
260
- }),
261
- ).toBe(false)
262
- expect(
263
- set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
264
- ).toBe(false)
265
- })
266
- })
267
-
268
- describe('assertRpc combined-aud', () => {
269
- it('allows did#serviceId aud when scope grants the same combined form', () => {
270
- const set = new ScopePermissions(
271
- 'rpc:app.bsky.feed.getFeed?aud=did:web:example.com%23bsky_appview',
272
- )
273
- expect(() =>
274
- set.assertRpc({
275
- aud: 'did:web:example.com#bsky_appview',
276
- lxm: 'app.bsky.feed.getFeed',
277
- }),
278
- ).not.toThrow()
279
- })
280
-
281
- it('rejects bare-DID aud when scope grants a combined form', () => {
282
- const set = new ScopePermissions(
283
- 'rpc:app.bsky.feed.getFeed?aud=did:web:example.com%23bsky_appview',
284
- )
285
- expect(() =>
286
- set.assertRpc({
287
- aud: 'did:web:example.com',
288
- lxm: 'app.bsky.feed.getFeed',
289
- }),
290
- ).toThrow()
291
- })
292
-
293
- it('allows wildcard aud against a combined-form match', () => {
294
- const set = new ScopePermissions('rpc:app.bsky.feed.getFeed?aud=*')
295
- expect(() =>
296
- set.assertRpc({
297
- aud: 'did:web:example.com#bsky_appview',
298
- lxm: 'app.bsky.feed.getFeed',
299
- }),
300
- ).not.toThrow()
301
- })
302
- })
303
- })
@@ -1,91 +0,0 @@
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,187 +0,0 @@
1
- import { AccountPermission } from './account-permission.js'
2
-
3
- describe('AccountPermission', () => {
4
- describe('static', () => {
5
- describe('fromString', () => {
6
- it('should parse valid scope strings', () => {
7
- const scope1 = AccountPermission.fromString('account:email?action=read')
8
- expect(scope1).not.toBeNull()
9
- expect(scope1!.attr).toBe('email')
10
- expect(scope1!.action).toEqual(['read'])
11
-
12
- const scope2 = AccountPermission.fromString(
13
- 'account:repo?action=manage',
14
- )
15
- expect(scope2).not.toBeNull()
16
- expect(scope2!.attr).toBe('repo')
17
- expect(scope2!.action).toEqual(['manage'])
18
- })
19
-
20
- it('should parse scope without action (defaults to read)', () => {
21
- const scope = AccountPermission.fromString('account:status')
22
- expect(scope).not.toBeNull()
23
- expect(scope!.attr).toBe('status')
24
- expect(scope!.action).toEqual(['read'])
25
- })
26
-
27
- it('should reject invalid attribute names', () => {
28
- const scope = AccountPermission.fromString('account:invalid')
29
- expect(scope).toBeNull()
30
- })
31
-
32
- it('should reject invalid action names', () => {
33
- const scope = AccountPermission.fromString(
34
- 'account:email?action=invalid',
35
- )
36
- expect(scope).toBeNull()
37
- })
38
-
39
- it('should reject malformed scope strings', () => {
40
- expect(AccountPermission.fromString('invalid:email')).toBeNull()
41
- expect(AccountPermission.fromString('account')).toBeNull()
42
- expect(AccountPermission.fromString('')).toBeNull()
43
- expect(AccountPermission.fromString('account:')).toBeNull()
44
- })
45
- })
46
-
47
- describe('scopeNeededFor', () => {
48
- it('should return correct scope string for read actions', () => {
49
- expect(
50
- AccountPermission.scopeNeededFor({ attr: 'email', action: 'read' }),
51
- ).toBe('account:email')
52
- expect(
53
- AccountPermission.scopeNeededFor({ attr: 'repo', action: 'read' }),
54
- ).toBe('account:repo')
55
- expect(
56
- AccountPermission.scopeNeededFor({ attr: 'status', action: 'read' }),
57
- ).toBe('account:status')
58
- })
59
-
60
- it('should return correct scope string for manage actions', () => {
61
- expect(
62
- AccountPermission.scopeNeededFor({ attr: 'email', action: 'manage' }),
63
- ).toBe('account:email?action=manage')
64
- expect(
65
- AccountPermission.scopeNeededFor({ attr: 'repo', action: 'manage' }),
66
- ).toBe('account:repo?action=manage')
67
- expect(
68
- AccountPermission.scopeNeededFor({
69
- attr: 'status',
70
- action: 'manage',
71
- }),
72
- ).toBe('account:status?action=manage')
73
- })
74
- })
75
- })
76
-
77
- describe('instance', () => {
78
- describe('matches', () => {
79
- it('should match read action', () => {
80
- const scope = AccountPermission.fromString('account:email?action=read')
81
- expect(scope).not.toBeNull()
82
- expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
83
- })
84
-
85
- it('should match manage action', () => {
86
- const scope = AccountPermission.fromString('account:repo?action=manage')
87
- expect(scope).not.toBeNull()
88
- expect(scope!.matches({ attr: 'repo', action: 'manage' })).toBe(true)
89
- })
90
-
91
- it('should not match unspecified action', () => {
92
- const scope = AccountPermission.fromString('account:email?action=read')
93
- expect(scope).not.toBeNull()
94
- expect(scope!.matches({ attr: 'email', action: 'manage' })).toBe(false)
95
- })
96
-
97
- it('should not match different attribute', () => {
98
- const scope = AccountPermission.fromString('account:email?action=read')
99
- expect(scope).not.toBeNull()
100
- expect(scope!.matches({ attr: 'repo', action: 'read' })).toBe(false)
101
- })
102
-
103
- it('should default to "read" action', () => {
104
- const scope = AccountPermission.fromString('account:email')
105
- expect(scope).not.toBeNull()
106
- expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
107
- expect(scope!.matches({ attr: 'email', action: 'manage' })).toBe(false)
108
- })
109
-
110
- it('should work with all valid attributes', () => {
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(
118
- 'account:status?action=read',
119
- )
120
-
121
- expect(emailScope).not.toBeNull()
122
- expect(repoScope).not.toBeNull()
123
- expect(statusScope).not.toBeNull()
124
-
125
- expect(emailScope!.matches({ attr: 'email', action: 'read' })).toBe(
126
- true,
127
- )
128
- expect(repoScope!.matches({ attr: 'repo', action: 'manage' })).toBe(
129
- true,
130
- )
131
- expect(statusScope!.matches({ attr: 'status', action: 'read' })).toBe(
132
- true,
133
- )
134
- })
135
-
136
- it('should allow read when "manage" action is specified', () => {
137
- const scope = AccountPermission.fromString(
138
- 'account:email?action=manage',
139
- )
140
- expect(scope).not.toBeNull()
141
- expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
142
- })
143
- })
144
-
145
- describe('toString', () => {
146
- it('should format scope with explicit action', () => {
147
- const scope = new AccountPermission('email', ['manage'])
148
- expect(scope.toString()).toBe('account:email?action=manage')
149
- })
150
-
151
- it('should format scope with default action', () => {
152
- const scope = new AccountPermission('repo', ['read'])
153
- expect(scope.toString()).toBe('account:repo')
154
- })
155
-
156
- it('should format all attributes correctly', () => {
157
- expect(new AccountPermission('email', ['read']).toString()).toBe(
158
- 'account:email',
159
- )
160
- expect(new AccountPermission('repo', ['read']).toString()).toBe(
161
- 'account:repo',
162
- )
163
- expect(new AccountPermission('status', ['read']).toString()).toBe(
164
- 'account:status',
165
- )
166
- expect(new AccountPermission('email', ['manage']).toString()).toBe(
167
- 'account:email?action=manage',
168
- )
169
- })
170
- })
171
- })
172
-
173
- it('should maintain consistency between toString and fromString', () => {
174
- const testCases = [
175
- 'account:email',
176
- 'account:email?action=manage',
177
- 'account:repo',
178
- 'account:repo?action=manage',
179
- 'account:status',
180
- 'account:status?action=manage',
181
- ]
182
-
183
- for (const scope of testCases) {
184
- expect(AccountPermission.fromString(scope)?.toString()).toBe(scope)
185
- }
186
- })
187
- })
@@ -1,78 +0,0 @@
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 { NeRoArray, 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: NeRoArray<AccountAction>,
28
- ) {}
29
-
30
- matches(options: AccountPermissionMatch) {
31
- return (
32
- this.attr === options.attr &&
33
- (this.action.includes('manage') || this.action.includes(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: true,
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({
74
- attr: options.attr,
75
- action: [options.action],
76
- })
77
- }
78
- }