@atproto/oauth-scopes 0.0.1 → 0.1.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 +31 -0
  2. package/dist/atproto-oauth-scope.d.ts +12 -0
  3. package/dist/atproto-oauth-scope.d.ts.map +1 -0
  4. package/dist/atproto-oauth-scope.js +32 -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 +4 -1
  43. package/dist/lib/util.d.ts.map +1 -1
  44. package/dist/lib/util.js +4 -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} +8 -6
  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 +43 -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 +7 -12
  96. package/src/{permission-set-transition.test.ts → scope-permissions-transition.test.ts} +34 -21
  97. package/src/{permission-set-transition.ts → scope-permissions-transition.ts} +16 -12
  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
@@ -0,0 +1,323 @@
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
+ })
@@ -0,0 +1,85 @@
1
+ import { AtprotoAudience, isAtprotoAudience } 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 { type AtprotoAudience, type Nsid, isAtprotoAudience, isNsid }
9
+
10
+ export type LxmParam = '*' | Nsid
11
+ export const isLxmParam = (value: unknown): value is LxmParam =>
12
+ value === '*' || isNsid(value)
13
+ export type AudParam = '*' | AtprotoAudience
14
+ export const isAudParam = (value: unknown): value is AudParam =>
15
+ value === '*' || isAtprotoAudience(value)
16
+
17
+ export type RpcPermissionMatch = {
18
+ lxm: string
19
+ aud: string
20
+ }
21
+
22
+ export class RpcPermission
23
+ implements ResourcePermission<'rpc', RpcPermissionMatch>
24
+ {
25
+ constructor(
26
+ public readonly aud: '*' | AtprotoAudience,
27
+ public readonly lxm: NeRoArray<'*' | Nsid>,
28
+ ) {}
29
+
30
+ matches(options: RpcPermissionMatch) {
31
+ const { aud, lxm } = this
32
+ return (
33
+ (aud === '*' || aud === options.aud) &&
34
+ (lxm.includes('*') || (lxm as readonly string[]).includes(options.lxm))
35
+ )
36
+ }
37
+
38
+ toString() {
39
+ return RpcPermission.parser.format(this)
40
+ }
41
+
42
+ protected static readonly parser = new Parser(
43
+ 'rpc',
44
+ {
45
+ lxm: {
46
+ multiple: true,
47
+ required: true,
48
+ validate: isLxmParam,
49
+ normalize: (value) =>
50
+ value.length > 1 && value.includes('*')
51
+ ? (['*'] as const)
52
+ : ([...new Set(value)].sort() as [Nsid, ...Nsid[]]),
53
+ },
54
+ aud: {
55
+ multiple: false,
56
+ required: true,
57
+ validate: isAudParam,
58
+ },
59
+ },
60
+ 'lxm',
61
+ )
62
+
63
+ static fromString(scope: string): RpcPermission | null {
64
+ if (!isScopeStringFor(scope, 'rpc')) return null
65
+ const syntax = ScopeStringSyntax.fromString(scope)
66
+ return RpcPermission.fromSyntax(syntax)
67
+ }
68
+
69
+ static fromSyntax(syntax: ScopeSyntax<'rpc'>): RpcPermission | null {
70
+ const result = RpcPermission.parser.parse(syntax)
71
+ if (!result) return null
72
+
73
+ // rpc:*?aud=* is forbidden
74
+ if (result.aud === '*' && result.lxm.includes('*')) return null
75
+
76
+ return new RpcPermission(result.aud, result.lxm)
77
+ }
78
+
79
+ static scopeNeededFor(options: RpcPermissionMatch): string {
80
+ return RpcPermission.parser.format({
81
+ aud: options.aud as AtprotoAudience,
82
+ lxm: [options.lxm as Nsid],
83
+ })
84
+ }
85
+ }
@@ -22,19 +22,19 @@ describe('ScopesSet', () => {
22
22
  })
23
23
 
24
24
  it('should match included scopes', () => {
25
- const set = new ScopesSet(['repo:foo.bar'])
25
+ const set = new ScopesSet(['repo:com.example.foo'])
26
26
  expect(
27
- set.matches('repo', { action: 'create', collection: 'foo.bar' }),
27
+ set.matches('repo', { action: 'create', collection: 'com.example.foo' }),
28
28
  ).toBe(true)
29
29
  expect(
30
- set.matches('repo', { action: 'create', collection: 'baz.qux' }),
30
+ set.matches('repo', { action: 'create', collection: 'com.example.bar' }),
31
31
  ).toBe(false)
32
32
  })
33
33
 
34
34
  it('should not match missing scopes', () => {
35
- const set = new ScopesSet(['repo:foo.bar?action=create'])
35
+ const set = new ScopesSet(['repo:com.example.foo?action=create'])
36
36
  expect(
37
- set.matches('repo', { action: 'delete', collection: 'foo.bar' }),
37
+ set.matches('repo', { action: 'delete', collection: 'com.example.foo' }),
38
38
  ).toBe(false)
39
39
  })
40
40
 
package/src/scopes-set.ts CHANGED
@@ -1,12 +1,32 @@
1
1
  import { ScopeMissingError } from './scope-missing-error.js'
2
2
  import {
3
- ScopeMatchingOptionsByResource,
4
- scopeMatches,
5
- scopeNeededFor,
6
- } from './utilities.js'
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'
7
19
 
8
20
  export { ScopeMissingError }
9
21
 
22
+ export type ScopeMatchingOptionsByResource = {
23
+ account: AccountPermissionMatch
24
+ identity: IdentityPermissionMatch
25
+ repo: RepoPermissionMatch
26
+ rpc: RpcPermissionMatch
27
+ blob: BlobPermissionMatch
28
+ }
29
+
10
30
  /**
11
31
  * Utility class to manage a set of scopes and check if they match specific
12
32
  * options for a given resource.
@@ -21,7 +41,7 @@ export class ScopesSet extends Set<string> {
21
41
  options: ScopeMatchingOptionsByResource[R],
22
42
  ): boolean {
23
43
  for (const scope of this) {
24
- if (scopeMatches(scope, resource, options)) return true
44
+ if (permissionScopeMatches(scope, resource, options)) return true
25
45
  }
26
46
  return false
27
47
  }
@@ -58,3 +78,57 @@ export class ScopesSet extends Set<string> {
58
78
  return new ScopesSet(string?.split(' '))
59
79
  }
60
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
+ }
@@ -1 +1 @@
1
- {"root":["./src/index.ts","./src/parser.ts","./src/permission-set-transition.ts","./src/permission-set.ts","./src/scope-missing-error.ts","./src/scopes-set.ts","./src/syntax.ts","./src/utilities.ts","./src/lib/did.ts","./src/lib/mime.ts","./src/lib/nsid.ts","./src/lib/util.ts","./src/resources/account-scope.ts","./src/resources/blob-scope.ts","./src/resources/identity-scope.ts","./src/resources/repo-scope.ts","./src/resources/rpc-scope.ts"],"version":"5.8.3"}
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"],"version":"5.8.3"}
@@ -1 +1 @@
1
- {"root":["./src/index.ts","./src/parser.ts","./src/permission-set-transition.test.ts","./src/permission-set-transition.ts","./src/permission-set.test.ts","./src/permission-set.ts","./src/scope-missing-error.ts","./src/scopes-set.test.ts","./src/scopes-set.ts","./src/syntax.test.ts","./src/syntax.ts","./src/utilities.ts","./src/lib/did.ts","./src/lib/mime.test.ts","./src/lib/mime.ts","./src/lib/nsid.ts","./src/lib/util.ts","./src/resources/account-scope.test.ts","./src/resources/account-scope.ts","./src/resources/blob-scope.test.ts","./src/resources/blob-scope.ts","./src/resources/identity-scope.test.ts","./src/resources/identity-scope.ts","./src/resources/repo-scope.test.ts","./src/resources/repo-scope.ts","./src/resources/rpc-scope.test.ts","./src/resources/rpc-scope.ts"],"version":"5.8.3"}
1
+ {"root":["./src/atproto-oauth-scope.ts","./src/index.ts","./src/scope-missing-error.ts","./src/scope-permissions-transition.test.ts","./src/scope-permissions-transition.ts","./src/scope-permissions.test.ts","./src/scope-permissions.ts","./src/scopes-set.test.ts","./src/scopes-set.ts","./src/lib/lexicon.ts","./src/lib/mime.test.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.test.ts","./src/lib/syntax-string.ts","./src/lib/syntax.test.ts","./src/lib/syntax.ts","./src/lib/util.ts","./src/scopes/account-permission.test.ts","./src/scopes/account-permission.ts","./src/scopes/blob-permission.test.ts","./src/scopes/blob-permission.ts","./src/scopes/identity-permission.test.ts","./src/scopes/identity-permission.ts","./src/scopes/include-scope.test.ts","./src/scopes/include-scope.ts","./src/scopes/repo-permission.test.ts","./src/scopes/repo-permission.ts","./src/scopes/rpc-permission.test.ts","./src/scopes/rpc-permission.ts"],"version":"5.8.3"}
package/dist/lib/did.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export type DIDLike = `did:${string}`;
2
- export declare const isDIDLike: (value: string) => value is DIDLike;
3
- //# sourceMappingURL=did.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"did.d.ts","sourceRoot":"","sources":["../../src/lib/did.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,OAAO,MAAM,EAAE,CAAA;AACrC,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,KAAG,KAAK,IAAI,OACzB,CAAA"}
package/dist/lib/did.js DELETED
@@ -1,6 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isDIDLike = void 0;
4
- const isDIDLike = (value) => value.startsWith('did:');
5
- exports.isDIDLike = isDIDLike;
6
- //# sourceMappingURL=did.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"did.js","sourceRoot":"","sources":["../../src/lib/did.ts"],"names":[],"mappings":";;;AACO,MAAM,SAAS,GAAG,CAAC,KAAa,EAAoB,EAAE,CAC3D,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;AADb,QAAA,SAAS,aACI"}
package/dist/parser.d.ts DELETED
@@ -1,31 +0,0 @@
1
- import { NeRoArray, ResourceSyntax, ScopeForResource } from './syntax.js';
2
- type InferStringPredicate<T extends undefined | ((value: string) => boolean)> = T extends ((value: string) => value is infer U extends string) ? U : string;
3
- type ParamsSchema = Record<string, {
4
- multiple: false;
5
- required: boolean;
6
- default?: string;
7
- normalize?: (value: string) => string;
8
- validate?: (value: string) => boolean;
9
- } | {
10
- multiple: true;
11
- required: boolean;
12
- default?: NeRoArray<string>;
13
- normalize?: (value: NeRoArray<string>) => NeRoArray<string>;
14
- validate?: (value: string) => boolean;
15
- }>;
16
- type ParsedParams<S extends ParamsSchema> = {
17
- [K in keyof S]: (S[K]['required'] extends true ? never : 'default' extends keyof S[K] ? S[K]['default'] : undefined) | (S[K]['multiple'] extends true ? NeRoArray<InferStringPredicate<S[K]['validate']>> : InferStringPredicate<S[K]['validate']>);
18
- } & NonNullable<unknown>;
19
- export declare class Parser<R extends string, S extends ParamsSchema> {
20
- readonly resource: R;
21
- readonly schema: S;
22
- readonly positionalName?: (keyof S & string) | undefined;
23
- readonly schemaKeys: ReadonlyArray<keyof S & string>;
24
- constructor(resource: R, schema: S, positionalName?: (keyof S & string) | undefined);
25
- format(values: ParsedParams<S>): ScopeForResource<R>;
26
- parse(syntax: ResourceSyntax): { [K in keyof S]: (S[K]["required"] extends true ? never : "default" extends keyof S[K] ? S[K][keyof S[K] & "default"] : undefined) | (S[K]["multiple"] extends true ? NeRoArray<InferStringPredicate<S[K]["validate"]>> : InferStringPredicate<S[K]["validate"]>); } | null;
27
- parseString(scope: string): ParsedParams<S> | null;
28
- }
29
- export declare function knownValuesValidator<T extends string>(values: Iterable<T>): (value: string) => value is T;
30
- export {};
31
- //# sourceMappingURL=parser.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,cAAc,EACd,gBAAgB,EAEjB,MAAM,aAAa,CAAA;AAEpB,KAAK,oBAAoB,CAAC,CAAC,SAAS,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAC1E,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,IAAI,MAAM,CAAC,SAAS,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAA;AAE7E,KAAK,YAAY,GAAG,MAAM,CACxB,MAAM,EACJ;IACE,QAAQ,EAAE,KAAK,CAAA;IACf,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACrC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAA;CACtC,GACD;IACE,QAAQ,EAAE,IAAI,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;IAC3B,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC,MAAM,CAAC,CAAA;IAC3D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAA;CACtC,CACJ,CAAA;AAED,KAAK,YAAY,CAAC,CAAC,SAAS,YAAY,IAAI;KACzC,CAAC,IAAI,MAAM,CAAC,GACT,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,IAAI,GAC1B,KAAK,GACL,SAAS,SAAS,MAAM,CAAC,CAAC,CAAC,CAAC,GAC1B,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GACf,SAAS,CAAC,GAChB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,IAAI,GAC1B,SAAS,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GACjD,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;CAChD,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;AAExB,qBAAa,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,YAAY;IAIxD,QAAQ,CAAC,QAAQ,EAAE,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,cAAc,CAAC,GAAE,MAAM,CAAC,GAAG,MAAM;IAL5C,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAA;gBAGzC,QAAQ,EAAE,CAAC,EACX,MAAM,EAAE,CAAC,EACT,cAAc,CAAC,GAAE,MAAM,CAAC,GAAG,MAAM,aAAA;IAK5C,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC;IAuCpD,KAAK,CAAC,MAAM,EAAE,cAAc,MA7D3B,CAAC;IAgGF,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI;CAInD;AAiBD,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,IAEhE,OAAO,MAAM,KAAG,KAAK,IAAI,CAAC,CACnC"}