@atproto/oauth-scopes 0.5.2 → 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.
- package/CHANGELOG.md +12 -0
- package/package.json +10 -6
- package/jest.config.cjs +0 -14
- package/src/atproto-oauth-scope.ts +0 -79
- package/src/index.ts +0 -13
- package/src/lib/lexicon.ts +0 -21
- package/src/lib/mime.test.ts +0 -98
- package/src/lib/mime.ts +0 -71
- package/src/lib/nsid.ts +0 -5
- package/src/lib/parser.ts +0 -176
- package/src/lib/resource-permission.ts +0 -10
- package/src/lib/syntax-lexicon.ts +0 -55
- package/src/lib/syntax-string.test.ts +0 -130
- package/src/lib/syntax-string.ts +0 -132
- package/src/lib/syntax.test.ts +0 -43
- package/src/lib/syntax.ts +0 -54
- package/src/lib/util.ts +0 -18
- package/src/scope-missing-error.ts +0 -15
- package/src/scope-permissions-transition.test.ts +0 -122
- package/src/scope-permissions-transition.ts +0 -71
- package/src/scope-permissions.test.ts +0 -303
- package/src/scope-permissions.ts +0 -91
- package/src/scopes/account-permission.test.ts +0 -187
- package/src/scopes/account-permission.ts +0 -78
- package/src/scopes/blob-permission.test.ts +0 -126
- package/src/scopes/blob-permission.ts +0 -105
- package/src/scopes/identity-permission.test.ts +0 -80
- package/src/scopes/identity-permission.ts +0 -54
- package/src/scopes/include-scope.test.ts +0 -637
- package/src/scopes/include-scope.ts +0 -208
- package/src/scopes/repo-permission.test.ts +0 -267
- package/src/scopes/repo-permission.ts +0 -111
- package/src/scopes/rpc-permission.test.ts +0 -323
- package/src/scopes/rpc-permission.ts +0 -90
- package/src/scopes-set.test.ts +0 -47
- package/src/scopes-set.ts +0 -134
- package/tsconfig.build.json +0 -9
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
import { RpcPermission } from './rpc-permission.js'
|
|
2
|
-
|
|
3
|
-
describe('RpcPermission', () => {
|
|
4
|
-
describe('static', () => {
|
|
5
|
-
describe('fromString', () => {
|
|
6
|
-
it('should parse positional scope', () => {
|
|
7
|
-
const scope = RpcPermission.fromString(
|
|
8
|
-
'rpc:com.example.service?aud=did:web:example.com%23service_id',
|
|
9
|
-
)
|
|
10
|
-
expect(scope).not.toBeNull()
|
|
11
|
-
expect(scope!.aud).toBe('did:web:example.com#service_id')
|
|
12
|
-
expect(scope!.lxm).toEqual(['com.example.service'])
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('should parse strings correctly', () => {
|
|
16
|
-
expect(
|
|
17
|
-
RpcPermission.fromString('rpc?lxm=com.example.method1&aud=*'),
|
|
18
|
-
).toEqual({
|
|
19
|
-
aud: '*',
|
|
20
|
-
lxm: ['com.example.method1'],
|
|
21
|
-
})
|
|
22
|
-
expect(
|
|
23
|
-
RpcPermission.fromString('rpc:com.example.method1?aud=*'),
|
|
24
|
-
).toEqual({
|
|
25
|
-
aud: '*',
|
|
26
|
-
lxm: ['com.example.method1'],
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should render strings correctly', () => {
|
|
31
|
-
expect(
|
|
32
|
-
new RpcPermission('did:web:example.com#service_id', [
|
|
33
|
-
'com.example.service',
|
|
34
|
-
]).toString(),
|
|
35
|
-
).toBe('rpc:com.example.service?aud=did:web:example.com%23service_id')
|
|
36
|
-
expect(new RpcPermission('*', ['com.example.method1']).toString()).toBe(
|
|
37
|
-
'rpc:com.example.method1?aud=*',
|
|
38
|
-
)
|
|
39
|
-
expect(
|
|
40
|
-
new RpcPermission('did:web:example.com#service_id', [
|
|
41
|
-
'com.example.method1',
|
|
42
|
-
'com.example.method2',
|
|
43
|
-
]).toString(),
|
|
44
|
-
).toBe(
|
|
45
|
-
'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=did:web:example.com%23service_id',
|
|
46
|
-
)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('should reject scopes without lxm', () => {
|
|
50
|
-
expect(
|
|
51
|
-
RpcPermission.fromString('rpc?aud=did:web:example.com%23service_id'),
|
|
52
|
-
).toBeNull()
|
|
53
|
-
expect(
|
|
54
|
-
RpcPermission.fromString('rpc:?aud=did:web:example.com%23service_id'),
|
|
55
|
-
).toBeNull()
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('should reject scopes without aud', () => {
|
|
59
|
-
expect(
|
|
60
|
-
RpcPermission.fromString('rpc?lxm=com.example.method1'),
|
|
61
|
-
).toBeNull()
|
|
62
|
-
expect(RpcPermission.fromString('rpc:com.example.method1')).toBeNull()
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('should reject scopes with lxm in both positional and query form', () => {
|
|
66
|
-
expect(
|
|
67
|
-
RpcPermission.fromString(
|
|
68
|
-
'rpc:com.example.method1?aud=did:web:example.com&lxm=com.example.method2',
|
|
69
|
-
),
|
|
70
|
-
).toBeNull()
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('should parse valid rpc scope with multiple lxm', () => {
|
|
74
|
-
const scope = RpcPermission.fromString(
|
|
75
|
-
'rpc?aud=*&lxm=com.example.method1&lxm=com.example.method2',
|
|
76
|
-
)
|
|
77
|
-
expect(scope).not.toBeNull()
|
|
78
|
-
expect(scope!.aud).toBe('*')
|
|
79
|
-
expect(scope!.lxm).toEqual([
|
|
80
|
-
'com.example.method1',
|
|
81
|
-
'com.example.method2',
|
|
82
|
-
])
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('should reject rpc scope without lxm', () => {
|
|
86
|
-
const scope = RpcPermission.fromString('rpc?aud=did:web:example.com')
|
|
87
|
-
expect(scope).toBeNull()
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('should reject rpc scope without aud', () => {
|
|
91
|
-
const scope = RpcPermission.fromString('rpc?lxm=com.example.method1')
|
|
92
|
-
expect(scope).toBeNull()
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('should reject any aud/any lxm', () => {
|
|
96
|
-
expect(RpcPermission.fromString('rpc?aud=*&lxm=*')).toBeNull()
|
|
97
|
-
expect(RpcPermission.fromString('rpc:*?aud=*')).toBeNull()
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('should reject missing aud', () => {
|
|
101
|
-
expect(RpcPermission.fromString('rpc:com.example.service')).toBeNull()
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('should reject invalid aud', () => {
|
|
105
|
-
expect(
|
|
106
|
-
RpcPermission.fromString('rpc:com.example.service?aud=invalid'),
|
|
107
|
-
).toBeNull()
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('should reject invalid lxm', () => {
|
|
111
|
-
expect(RpcPermission.fromString('rpc:invalid')).toBeNull()
|
|
112
|
-
expect(RpcPermission.fromString('rpc?lxm=invalid')).toBeNull()
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
for (const invalid of [
|
|
116
|
-
'rpc:*',
|
|
117
|
-
'invalid',
|
|
118
|
-
'rpc:invalid',
|
|
119
|
-
'rpc:invalid?aud=did:web:example.com',
|
|
120
|
-
'rpc:invalid?aud=did:web:example.com%23service_id',
|
|
121
|
-
'rpc:foo.bar',
|
|
122
|
-
'rpc:com.example.service?aud=did:web:example.com%23service_id&invalid=param',
|
|
123
|
-
'rpc:foo.bar.baz?aud=did:web',
|
|
124
|
-
'rpc:foo.bar.baz?aud=did:web%23service_id',
|
|
125
|
-
'rpc:foo.bar.baz?aud=did:plc:111',
|
|
126
|
-
'rpc:foo.bar.baz?aud=did:plc:111%23service_id',
|
|
127
|
-
'rpc:foo.bar.baz?aud=did:foo:bar',
|
|
128
|
-
'rpc:foo.bar.baz?aud=did:foo:bar%23service_id',
|
|
129
|
-
'rpc:foo.bar.baz?aud=did:web:example.com%23service_id&lxm=foo.bar.baz',
|
|
130
|
-
'rpc:foo.bar.baz?aud=invalid',
|
|
131
|
-
'rpc:foo.bar.baz?aud=invalid',
|
|
132
|
-
'rpc:invalid?aud=did:web:example.com',
|
|
133
|
-
'rpc:invalid?aud=did:web:example.com%23service_id',
|
|
134
|
-
'rpc:com.example.service?aud=invalid',
|
|
135
|
-
'notrpc:com.example.service?aud=did:web:example.com%23service_id',
|
|
136
|
-
'rpc?lxm=invalid&aud=invalid',
|
|
137
|
-
]) {
|
|
138
|
-
it(`should return null for invalid rpc scope: ${invalid}`, () => {
|
|
139
|
-
expect(RpcPermission.fromString(invalid)).toBeNull()
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
describe('scopeNeededFor', () => {
|
|
145
|
-
it('should return correct scope string for specific lxm and aud', () => {
|
|
146
|
-
const scope = RpcPermission.scopeNeededFor({
|
|
147
|
-
lxm: 'com.example.service',
|
|
148
|
-
aud: 'did:web:example.com#service_id',
|
|
149
|
-
})
|
|
150
|
-
expect(scope).toBe(
|
|
151
|
-
'rpc:com.example.service?aud=did:web:example.com%23service_id',
|
|
152
|
-
)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('should return scope that accepts all aud with specific lxm', () => {
|
|
156
|
-
const scope = RpcPermission.scopeNeededFor({
|
|
157
|
-
lxm: 'com.example.method1',
|
|
158
|
-
aud: '*',
|
|
159
|
-
})
|
|
160
|
-
expect(scope).toBe('rpc:com.example.method1?aud=*')
|
|
161
|
-
})
|
|
162
|
-
})
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
describe('instance', () => {
|
|
166
|
-
describe('matches', () => {
|
|
167
|
-
it('should match exact lxm and aud', () => {
|
|
168
|
-
const scope = RpcPermission.fromString(
|
|
169
|
-
'rpc:com.example.service?aud=did:web:example.com%23service_id',
|
|
170
|
-
)
|
|
171
|
-
expect(scope).not.toBeNull()
|
|
172
|
-
expect(
|
|
173
|
-
scope!.matches({
|
|
174
|
-
lxm: 'com.example.service',
|
|
175
|
-
aud: 'did:web:example.com#service_id',
|
|
176
|
-
}),
|
|
177
|
-
).toBe(true)
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('should not match different lxm', () => {
|
|
181
|
-
const scope = RpcPermission.fromString(
|
|
182
|
-
'rpc:com.example.service?aud=did:web:example.com%23service_id',
|
|
183
|
-
)
|
|
184
|
-
expect(scope).not.toBeNull()
|
|
185
|
-
expect(
|
|
186
|
-
scope!.matches({
|
|
187
|
-
lxm: 'com.example.OtherService',
|
|
188
|
-
aud: 'did:web:example.com#service_id',
|
|
189
|
-
}),
|
|
190
|
-
).toBe(false)
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('should not match different aud', () => {
|
|
194
|
-
const scope = RpcPermission.fromString(
|
|
195
|
-
'rpc:com.example.service?aud=did:web:example.com%23service_id',
|
|
196
|
-
)
|
|
197
|
-
expect(scope).not.toBeNull()
|
|
198
|
-
expect(
|
|
199
|
-
scope!.matches({
|
|
200
|
-
lxm: 'com.example.service',
|
|
201
|
-
aud: 'did:example:456#service_id',
|
|
202
|
-
}),
|
|
203
|
-
).toBe(false)
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('should match wildcard aud', () => {
|
|
207
|
-
const scope = RpcPermission.fromString('rpc:com.example.method1?aud=*')
|
|
208
|
-
expect(scope).not.toBeNull()
|
|
209
|
-
expect(
|
|
210
|
-
scope!.matches({
|
|
211
|
-
lxm: 'com.example.method1',
|
|
212
|
-
aud: 'did:web:example.com#service_id',
|
|
213
|
-
}),
|
|
214
|
-
).toBe(true)
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
it('should match wildcard lxm', () => {
|
|
218
|
-
const scope = RpcPermission.fromString(
|
|
219
|
-
'rpc:*?aud=did:web:example.com%23service_id',
|
|
220
|
-
)
|
|
221
|
-
expect(scope).not.toBeNull()
|
|
222
|
-
expect(
|
|
223
|
-
scope!.matches({
|
|
224
|
-
lxm: 'com.example.method1',
|
|
225
|
-
aud: 'did:web:example.com#service_id',
|
|
226
|
-
}),
|
|
227
|
-
).toBe(true)
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('should not match different lxm with wildcard aud', () => {
|
|
231
|
-
const scope = RpcPermission.fromString(
|
|
232
|
-
'rpc:*?aud=did:web:example.com%23service_id',
|
|
233
|
-
)
|
|
234
|
-
expect(scope).not.toBeNull()
|
|
235
|
-
expect(
|
|
236
|
-
scope!.matches({
|
|
237
|
-
lxm: 'com.example.anyMethod',
|
|
238
|
-
aud: 'did:web:example.com#service_id',
|
|
239
|
-
}),
|
|
240
|
-
).toBe(true)
|
|
241
|
-
})
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
describe('toString', () => {
|
|
245
|
-
it('should format scope with lxm and aud', () => {
|
|
246
|
-
const scope = new RpcPermission('did:web:example.com#service_id', [
|
|
247
|
-
'com.example.service',
|
|
248
|
-
])
|
|
249
|
-
expect(scope.toString()).toBe(
|
|
250
|
-
'rpc:com.example.service?aud=did:web:example.com%23service_id',
|
|
251
|
-
)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('should format scope with wildcard aud', () => {
|
|
255
|
-
const scope = new RpcPermission('*', ['com.example.method1'])
|
|
256
|
-
expect(scope.toString()).toBe('rpc:com.example.method1?aud=*')
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('should format scope with wildcard lxm', () => {
|
|
260
|
-
const scope = new RpcPermission('did:web:example.com#service_id', ['*'])
|
|
261
|
-
expect(scope.toString()).toBe(
|
|
262
|
-
'rpc:*?aud=did:web:example.com%23service_id',
|
|
263
|
-
)
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
it('simplifies lxm if one of them is "*"', () => {
|
|
267
|
-
const scope = new RpcPermission('did:web:example.com#service_id', [
|
|
268
|
-
'*',
|
|
269
|
-
'com.example.method1',
|
|
270
|
-
])
|
|
271
|
-
expect(scope.toString()).toBe(
|
|
272
|
-
'rpc:*?aud=did:web:example.com%23service_id',
|
|
273
|
-
)
|
|
274
|
-
})
|
|
275
|
-
})
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
describe('consistency', () => {
|
|
279
|
-
const testCases: { input: string; expected: string }[] = [
|
|
280
|
-
{
|
|
281
|
-
input: 'rpc:com.example.service?aud=did:web:example.com%23service_id',
|
|
282
|
-
expected:
|
|
283
|
-
'rpc:com.example.service?aud=did:web:example.com%23service_id',
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
input: 'rpc:com.example.service?aud=did:web:example.com#service_id',
|
|
287
|
-
expected:
|
|
288
|
-
'rpc:com.example.service?aud=did:web:example.com%23service_id',
|
|
289
|
-
},
|
|
290
|
-
{
|
|
291
|
-
input: 'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=*',
|
|
292
|
-
expected: 'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=*',
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
input:
|
|
296
|
-
'rpc?lxm=com.example.method1&lxm=com.example.method2&lxm=*&aud=did:web:example.com%23service_id',
|
|
297
|
-
expected: 'rpc:*?aud=did:web:example.com%23service_id',
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
input: 'rpc?aud=did:web:example.com%23foo&lxm=com.example.service',
|
|
301
|
-
expected: 'rpc:com.example.service?aud=did:web:example.com%23foo',
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
input: 'rpc?lxm=com.example.method1&aud=did:web:example.com#foo',
|
|
305
|
-
expected: 'rpc:com.example.method1?aud=did:web:example.com%23foo',
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
input: 'rpc?lxm=com.example.method1&aud=did:web:example.com%23bar',
|
|
309
|
-
expected: 'rpc:com.example.method1?aud=did:web:example.com%23bar',
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
input: 'rpc:com.example.method1?&aud=*',
|
|
313
|
-
expected: 'rpc:com.example.method1?aud=*',
|
|
314
|
-
},
|
|
315
|
-
]
|
|
316
|
-
|
|
317
|
-
for (const { input, expected } of testCases) {
|
|
318
|
-
it(`should properly re-format ${input}`, () => {
|
|
319
|
-
expect(RpcPermission.fromString(input)?.toString()).toBe(expected)
|
|
320
|
-
})
|
|
321
|
-
}
|
|
322
|
-
})
|
|
323
|
-
})
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { AtprotoDidRefAbsolute, isAtprotoDidRefAbsolute } from '@atproto/did'
|
|
2
|
-
import { Nsid, isNsid } from '../lib/nsid.js'
|
|
3
|
-
import { Parser } from '../lib/parser.js'
|
|
4
|
-
import { ResourcePermission } from '../lib/resource-permission.js'
|
|
5
|
-
import { ScopeStringSyntax } from '../lib/syntax-string.js'
|
|
6
|
-
import { NeRoArray, ScopeSyntax, isScopeStringFor } from '../lib/syntax.js'
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
type AtprotoDidRefAbsolute,
|
|
10
|
-
type Nsid,
|
|
11
|
-
isAtprotoDidRefAbsolute,
|
|
12
|
-
isNsid,
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type LxmParam = '*' | Nsid
|
|
16
|
-
export const isLxmParam = (value: unknown): value is LxmParam =>
|
|
17
|
-
value === '*' || isNsid(value)
|
|
18
|
-
export type AudParam = '*' | AtprotoDidRefAbsolute
|
|
19
|
-
export const isAudParam = (value: unknown): value is AudParam =>
|
|
20
|
-
value === '*' || isAtprotoDidRefAbsolute(value)
|
|
21
|
-
|
|
22
|
-
export type RpcPermissionMatch = {
|
|
23
|
-
lxm: string
|
|
24
|
-
aud: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class RpcPermission
|
|
28
|
-
implements ResourcePermission<'rpc', RpcPermissionMatch>
|
|
29
|
-
{
|
|
30
|
-
constructor(
|
|
31
|
-
public readonly aud: AudParam,
|
|
32
|
-
public readonly lxm: NeRoArray<LxmParam>,
|
|
33
|
-
) {}
|
|
34
|
-
|
|
35
|
-
matches(options: RpcPermissionMatch) {
|
|
36
|
-
const { aud, lxm } = this
|
|
37
|
-
return (
|
|
38
|
-
(aud === '*' || aud === options.aud) &&
|
|
39
|
-
(lxm.includes('*') || (lxm as readonly string[]).includes(options.lxm))
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
toString() {
|
|
44
|
-
return RpcPermission.parser.format(this)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
protected static readonly parser = new Parser(
|
|
48
|
-
'rpc',
|
|
49
|
-
{
|
|
50
|
-
lxm: {
|
|
51
|
-
multiple: true,
|
|
52
|
-
required: true,
|
|
53
|
-
validate: isLxmParam,
|
|
54
|
-
normalize: (value) =>
|
|
55
|
-
value.length > 1 && value.includes('*')
|
|
56
|
-
? (['*'] as const)
|
|
57
|
-
: ([...new Set(value)].sort() as [Nsid, ...Nsid[]]),
|
|
58
|
-
},
|
|
59
|
-
aud: {
|
|
60
|
-
multiple: false,
|
|
61
|
-
required: true,
|
|
62
|
-
validate: isAudParam,
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
'lxm',
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
static fromString(scope: string): RpcPermission | null {
|
|
69
|
-
if (!isScopeStringFor(scope, 'rpc')) return null
|
|
70
|
-
const syntax = ScopeStringSyntax.fromString(scope)
|
|
71
|
-
return RpcPermission.fromSyntax(syntax)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
static fromSyntax(syntax: ScopeSyntax<'rpc'>): RpcPermission | null {
|
|
75
|
-
const result = RpcPermission.parser.parse(syntax)
|
|
76
|
-
if (!result) return null
|
|
77
|
-
|
|
78
|
-
// rpc:*?aud=* is forbidden
|
|
79
|
-
if (result.aud === '*' && result.lxm.includes('*')) return null
|
|
80
|
-
|
|
81
|
-
return new RpcPermission(result.aud, result.lxm)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
static scopeNeededFor(options: RpcPermissionMatch): string {
|
|
85
|
-
return RpcPermission.parser.format({
|
|
86
|
-
aud: options.aud as AtprotoDidRefAbsolute,
|
|
87
|
-
lxm: [options.lxm as Nsid],
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
}
|
package/src/scopes-set.test.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { ScopesSet } from './scopes-set.js'
|
|
2
|
-
|
|
3
|
-
describe('ScopesSet', () => {
|
|
4
|
-
it('should initialize with an empty set', () => {
|
|
5
|
-
const set = new ScopesSet()
|
|
6
|
-
expect(set.size).toBe(0)
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
it('should add scopes correctly', () => {
|
|
10
|
-
const set = new ScopesSet()
|
|
11
|
-
set.add('repo:read')
|
|
12
|
-
expect(set.size).toBe(1)
|
|
13
|
-
expect(set.has('repo:read')).toBe(true)
|
|
14
|
-
expect(set.has('repo:write')).toBe(false)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('should remove scopes correctly', () => {
|
|
18
|
-
const set = new ScopesSet(['repo:read'])
|
|
19
|
-
set.delete('repo:read')
|
|
20
|
-
expect(set.size).toBe(0)
|
|
21
|
-
expect(set.has('repo:read')).toBe(false)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('should match included scopes', () => {
|
|
25
|
-
const set = new ScopesSet(['repo:com.example.foo'])
|
|
26
|
-
expect(
|
|
27
|
-
set.matches('repo', { action: 'create', collection: 'com.example.foo' }),
|
|
28
|
-
).toBe(true)
|
|
29
|
-
expect(
|
|
30
|
-
set.matches('repo', { action: 'create', collection: 'com.example.bar' }),
|
|
31
|
-
).toBe(false)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('should not match missing scopes', () => {
|
|
35
|
-
const set = new ScopesSet(['repo:com.example.foo?action=create'])
|
|
36
|
-
expect(
|
|
37
|
-
set.matches('repo', { action: 'delete', collection: 'com.example.foo' }),
|
|
38
|
-
).toBe(false)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('should not match invalid scopes', () => {
|
|
42
|
-
const set = new ScopesSet(['repo:not-a-valid-nsid'])
|
|
43
|
-
expect(
|
|
44
|
-
set.matches('repo', { action: 'create', collection: 'not-a-valid-nsid' }),
|
|
45
|
-
).toBe(false)
|
|
46
|
-
})
|
|
47
|
-
})
|
package/src/scopes-set.ts
DELETED
|
@@ -1,134 +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
|
-
|
|
20
|
-
export { ScopeMissingError }
|
|
21
|
-
|
|
22
|
-
export type ScopeMatchingOptionsByResource = {
|
|
23
|
-
account: AccountPermissionMatch
|
|
24
|
-
identity: IdentityPermissionMatch
|
|
25
|
-
repo: RepoPermissionMatch
|
|
26
|
-
rpc: RpcPermissionMatch
|
|
27
|
-
blob: BlobPermissionMatch
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Utility class to manage a set of scopes and check if they match specific
|
|
32
|
-
* options for a given resource.
|
|
33
|
-
*/
|
|
34
|
-
export class ScopesSet extends Set<string> {
|
|
35
|
-
/**
|
|
36
|
-
* Check if the container has a scope that matches the given options for a
|
|
37
|
-
* specific resource.
|
|
38
|
-
*/
|
|
39
|
-
public matches<R extends keyof ScopeMatchingOptionsByResource>(
|
|
40
|
-
resource: R,
|
|
41
|
-
options: ScopeMatchingOptionsByResource[R],
|
|
42
|
-
): boolean {
|
|
43
|
-
for (const scope of this) {
|
|
44
|
-
if (permissionScopeMatches(scope, resource, options)) return true
|
|
45
|
-
}
|
|
46
|
-
return false
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
public assert<R extends keyof ScopeMatchingOptionsByResource>(
|
|
50
|
-
resource: R,
|
|
51
|
-
options: ScopeMatchingOptionsByResource[R],
|
|
52
|
-
) {
|
|
53
|
-
if (!this.matches(resource, options)) {
|
|
54
|
-
const scope = scopeNeededFor(resource, options)
|
|
55
|
-
throw new ScopeMissingError(scope)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
public some(fn: (scope: string) => boolean): boolean {
|
|
60
|
-
for (const scope of this) if (fn(scope)) return true
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public every(fn: (scope: string) => boolean): boolean {
|
|
65
|
-
for (const scope of this) if (!fn(scope)) return false
|
|
66
|
-
return true
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public *filter(fn: (scope: string) => boolean) {
|
|
70
|
-
for (const scope of this) if (fn(scope)) yield scope
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
public *map<O>(fn: (scope: string) => O) {
|
|
74
|
-
for (const scope of this) yield fn(scope)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
static fromString(string?: string): ScopesSet {
|
|
78
|
-
return new ScopesSet(string?.split(' '))
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function scopeNeededFor<R extends keyof ScopeMatchingOptionsByResource>(
|
|
83
|
-
resource: R,
|
|
84
|
-
options: ScopeMatchingOptionsByResource[R],
|
|
85
|
-
): string {
|
|
86
|
-
switch (resource) {
|
|
87
|
-
case 'account':
|
|
88
|
-
return AccountPermission.scopeNeededFor(options as AccountPermissionMatch)
|
|
89
|
-
case 'identity':
|
|
90
|
-
return IdentityPermission.scopeNeededFor(
|
|
91
|
-
options as IdentityPermissionMatch,
|
|
92
|
-
)
|
|
93
|
-
case 'repo':
|
|
94
|
-
return RepoPermission.scopeNeededFor(options as RepoPermissionMatch)
|
|
95
|
-
case 'rpc':
|
|
96
|
-
return RpcPermission.scopeNeededFor(options as RpcPermissionMatch)
|
|
97
|
-
case 'blob':
|
|
98
|
-
return BlobPermission.scopeNeededFor(options as BlobPermissionMatch)
|
|
99
|
-
}
|
|
100
|
-
// @ts-expect-error
|
|
101
|
-
throw new TypeError(`Unknown resource: ${resource}`)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function permissionScopeMatches<R extends keyof ScopeMatchingOptionsByResource>(
|
|
105
|
-
scope: string,
|
|
106
|
-
resource: R,
|
|
107
|
-
options: ScopeMatchingOptionsByResource[R],
|
|
108
|
-
): boolean {
|
|
109
|
-
// @NOTE we might want to cache the parsed scopes though, in practice, a
|
|
110
|
-
// single scope is unlikely to be parsed multiple times during a single
|
|
111
|
-
// request.
|
|
112
|
-
const permission = parsePermissionScope(resource, scope)
|
|
113
|
-
if (!permission) return false
|
|
114
|
-
|
|
115
|
-
// @ts-expect-error
|
|
116
|
-
return permission.matches(options)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function parsePermissionScope(resource: string, scope: string) {
|
|
120
|
-
switch (resource) {
|
|
121
|
-
case 'account':
|
|
122
|
-
return AccountPermission.fromString(scope)
|
|
123
|
-
case 'identity':
|
|
124
|
-
return IdentityPermission.fromString(scope)
|
|
125
|
-
case 'repo':
|
|
126
|
-
return RepoPermission.fromString(scope)
|
|
127
|
-
case 'rpc':
|
|
128
|
-
return RpcPermission.fromString(scope)
|
|
129
|
-
case 'blob':
|
|
130
|
-
return BlobPermission.fromString(scope)
|
|
131
|
-
default:
|
|
132
|
-
return null
|
|
133
|
-
}
|
|
134
|
-
}
|
package/tsconfig.build.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":"7.0.0-dev.20260614.1","root":["./src/atproto-oauth-scope.ts","./src/index.ts","./src/scope-missing-error.ts","./src/scope-permissions-transition.ts","./src/scope-permissions.ts","./src/scopes-set.ts","./src/lib/lexicon.ts","./src/lib/mime.ts","./src/lib/nsid.ts","./src/lib/parser.ts","./src/lib/resource-permission.ts","./src/lib/syntax-lexicon.ts","./src/lib/syntax-string.ts","./src/lib/syntax.ts","./src/lib/util.ts","./src/scopes/account-permission.ts","./src/scopes/blob-permission.ts","./src/scopes/identity-permission.ts","./src/scopes/include-scope.ts","./src/scopes/repo-permission.ts","./src/scopes/rpc-permission.ts"]}
|
package/tsconfig.json
DELETED