@atproto/oauth-scopes 0.0.2 → 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 +25 -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} +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 +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} +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,280 +0,0 @@
1
- import { RpcScope } from './rpc-scope.js'
2
-
3
- describe('RpcScope', () => {
4
- describe('static', () => {
5
- describe('fromString', () => {
6
- it('should parse positional scope', () => {
7
- const scope = RpcScope.fromString(
8
- 'rpc:com.example.service?aud=did:example:123',
9
- )
10
- expect(scope).not.toBeNull()
11
- expect(scope!.aud).toBe('did:example:123')
12
- expect(scope!.lxm).toEqual(['com.example.service'])
13
- })
14
-
15
- it('should parse strings correctly', () => {
16
- expect(
17
- RpcScope.fromString('rpc?lxm=com.example.method1&aud=*'),
18
- ).toEqual({
19
- aud: '*',
20
- lxm: ['com.example.method1'],
21
- })
22
- expect(RpcScope.fromString('rpc:com.example.method1?aud=*')).toEqual({
23
- aud: '*',
24
- lxm: ['com.example.method1'],
25
- })
26
- })
27
-
28
- it('should render strings correctly', () => {
29
- expect(
30
- new RpcScope('did:example:123', ['com.example.service']).toString(),
31
- ).toBe('rpc:com.example.service?aud=did:example:123')
32
- expect(new RpcScope('*', ['com.example.method1']).toString()).toBe(
33
- 'rpc:com.example.method1?aud=*',
34
- )
35
- expect(
36
- new RpcScope('did:example:123', [
37
- 'com.example.method1',
38
- 'com.example.method2',
39
- ]).toString(),
40
- ).toBe(
41
- 'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=did:example:123',
42
- )
43
- })
44
-
45
- it('should reject scopes without lxm', () => {
46
- expect(RpcScope.fromString('rpc?aud=did:example:123')).toBeNull()
47
- expect(RpcScope.fromString('rpc:?aud=did:example:123')).toBeNull()
48
- })
49
-
50
- it('should reject scopes without aud', () => {
51
- expect(RpcScope.fromString('rpc?lxm=com.example.method1')).toBeNull()
52
- expect(RpcScope.fromString('rpc:com.example.method1')).toBeNull()
53
- })
54
-
55
- it('should reject scopes with lxm in both positional and query form', () => {
56
- expect(
57
- RpcScope.fromString(
58
- 'rpc:com.example.method1?aud=did:example:123&lxm=com.example.method2',
59
- ),
60
- ).toBeNull()
61
- })
62
-
63
- it('should parse valid rpc scope with multiple lxm', () => {
64
- const scope = RpcScope.fromString(
65
- 'rpc?aud=*&lxm=com.example.method1&lxm=com.example.method2',
66
- )
67
- expect(scope).not.toBeNull()
68
- expect(scope!.aud).toBe('*')
69
- expect(scope!.lxm).toEqual([
70
- 'com.example.method1',
71
- 'com.example.method2',
72
- ])
73
- })
74
-
75
- it('should reject rpc scope without lxm', () => {
76
- const scope = RpcScope.fromString('rpc?aud=did:example:123')
77
- expect(scope).toBeNull()
78
- })
79
-
80
- it('should reject rpc scope without aud', () => {
81
- const scope = RpcScope.fromString('rpc?lxm=com.example.method1')
82
- expect(scope).toBeNull()
83
- })
84
-
85
- it('should reject any aud/any lxm', () => {
86
- expect(RpcScope.fromString('rpc?aud=*&lxm=*')).toBeNull()
87
- expect(RpcScope.fromString('rpc:*?aud=*')).toBeNull()
88
- })
89
-
90
- it('should reject missing aud', () => {
91
- expect(RpcScope.fromString('rpc:com.example.service')).toBeNull()
92
- })
93
-
94
- it('should reject invalid aud', () => {
95
- expect(
96
- RpcScope.fromString('rpc:com.example.service?aud=invalid'),
97
- ).toBeNull()
98
- })
99
-
100
- it('should reject invalid lxm', () => {
101
- expect(RpcScope.fromString('rpc:invalid')).toBeNull()
102
- expect(RpcScope.fromString('rpc?lxm=invalid')).toBeNull()
103
- })
104
-
105
- for (const invalid of [
106
- 'rpc:*',
107
- 'invalid',
108
- 'rpc:invalid',
109
- 'rpc:invalid?aud=did:foo:bar',
110
- 'rpc:foo.bar?aud=did:foo:bar&lxm=bar.baz',
111
- 'rpc:foo.bar?aud=invalid',
112
- 'rpc:invalid?aud=did:example:123',
113
- 'rpc:com.example.service?aud=invalid',
114
- 'notrpc:com.example.service?aud=did:example:123',
115
- 'rpc?lxm=invalid&aud=invalid',
116
- ]) {
117
- it(`should return null for invalid rpc scope: ${invalid}`, () => {
118
- expect(RpcScope.fromString(invalid)).toBeNull()
119
- })
120
- }
121
- })
122
-
123
- describe('scopeNeededFor', () => {
124
- it('should return correct scope string for specific lxm and aud', () => {
125
- const scope = RpcScope.scopeNeededFor({
126
- lxm: 'com.example.service',
127
- aud: 'did:example:123',
128
- })
129
- expect(scope).toBe('rpc:com.example.service?aud=did:example:123')
130
- })
131
-
132
- it('should return scope that accepts all aud with specific lxm', () => {
133
- const scope = RpcScope.scopeNeededFor({
134
- lxm: 'com.example.method1',
135
- aud: '*',
136
- })
137
- expect(scope).toBe('rpc:com.example.method1?aud=*')
138
- })
139
- })
140
- })
141
-
142
- describe('instance', () => {
143
- describe('matches', () => {
144
- it('should match exact lxm and aud', () => {
145
- const scope = RpcScope.fromString(
146
- 'rpc:com.example.service?aud=did:example:123',
147
- )
148
- expect(scope).not.toBeNull()
149
- expect(
150
- scope!.matches({
151
- lxm: 'com.example.service',
152
- aud: 'did:example:123',
153
- }),
154
- ).toBe(true)
155
- })
156
-
157
- it('should not match different lxm', () => {
158
- const scope = RpcScope.fromString(
159
- 'rpc:com.example.service?aud=did:example:123',
160
- )
161
- expect(scope).not.toBeNull()
162
- expect(
163
- scope!.matches({
164
- lxm: 'com.example.OtherService',
165
- aud: 'did:example:123',
166
- }),
167
- ).toBe(false)
168
- })
169
-
170
- it('should not match different aud', () => {
171
- const scope = RpcScope.fromString(
172
- 'rpc:com.example.service?aud=did:example:123',
173
- )
174
- expect(scope).not.toBeNull()
175
- expect(
176
- scope!.matches({
177
- lxm: 'com.example.service',
178
- aud: 'did:example:456',
179
- }),
180
- ).toBe(false)
181
- })
182
-
183
- it('should match wildcard aud', () => {
184
- const scope = RpcScope.fromString('rpc:com.example.method1?aud=*')
185
- expect(scope).not.toBeNull()
186
- expect(
187
- scope!.matches({
188
- lxm: 'com.example.method1',
189
- aud: 'did:example:123',
190
- }),
191
- ).toBe(true)
192
- })
193
-
194
- it('should match wildcard lxm', () => {
195
- const scope = RpcScope.fromString('rpc:*?aud=did:example:123')
196
- expect(scope).not.toBeNull()
197
- expect(
198
- scope!.matches({
199
- lxm: 'com.example.method1',
200
- aud: 'did:example:123',
201
- }),
202
- ).toBe(true)
203
- })
204
-
205
- it('should not match different lxm with wildcard aud', () => {
206
- const scope = RpcScope.fromString('rpc:*?aud=did:example:123')
207
- expect(scope).not.toBeNull()
208
- expect(
209
- scope!.matches({
210
- lxm: 'com.example.anyMethod',
211
- aud: 'did:example:123',
212
- }),
213
- ).toBe(true)
214
- })
215
- })
216
-
217
- describe('toString', () => {
218
- it('should format scope with lxm and aud', () => {
219
- const scope = new RpcScope('did:example:123', ['com.example.service'])
220
- expect(scope.toString()).toBe(
221
- 'rpc:com.example.service?aud=did:example:123',
222
- )
223
- })
224
-
225
- it('should format scope with wildcard aud', () => {
226
- const scope = new RpcScope('*', ['com.example.method1'])
227
- expect(scope.toString()).toBe('rpc:com.example.method1?aud=*')
228
- })
229
-
230
- it('should format scope with wildcard lxm', () => {
231
- const scope = new RpcScope('did:example:123', ['*'])
232
- expect(scope.toString()).toBe('rpc:*?aud=did:example:123')
233
- })
234
-
235
- it('simplifies lxm if one of them is "*"', () => {
236
- const scope = new RpcScope('did:example:123', [
237
- '*',
238
- 'com.example.method1',
239
- ])
240
- expect(scope.toString()).toBe('rpc:*?aud=did:example:123')
241
- })
242
- })
243
- })
244
-
245
- describe('consistency', () => {
246
- const testCases: { input: string; expected: string }[] = [
247
- {
248
- input: 'rpc:com.example.service?aud=did:example:123',
249
- expected: 'rpc:com.example.service?aud=did:example:123',
250
- },
251
- {
252
- input: 'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=*',
253
- expected: 'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=*',
254
- },
255
- {
256
- input:
257
- 'rpc?lxm=com.example.method1&lxm=com.example.method2&lxm=*&aud=did:example:123',
258
- expected: 'rpc:*?aud=did:example:123',
259
- },
260
- {
261
- input: 'rpc?aud=did:example:123&lxm=com.example.service',
262
- expected: 'rpc:com.example.service?aud=did:example:123',
263
- },
264
- {
265
- input: 'rpc?lxm=com.example.method1&aud=did:example:123',
266
- expected: 'rpc:com.example.method1?aud=did:example:123',
267
- },
268
- {
269
- input: 'rpc:com.example.method1?&aud=*',
270
- expected: 'rpc:com.example.method1?aud=*',
271
- },
272
- ]
273
-
274
- for (const { input, expected } of testCases) {
275
- it(`should properly re-format ${input}`, () => {
276
- expect(RpcScope.fromString(input)?.toString()).toBe(expected)
277
- })
278
- }
279
- })
280
- })
@@ -1,77 +0,0 @@
1
- import { DIDLike, isDIDLike } from '../lib/did.js'
2
- import { NSID, isNSID } from '../lib/nsid.js'
3
- import { Parser } from '../parser.js'
4
- import { NeRoArray, ResourceSyntax, isScopeForResource } from '../syntax.js'
5
-
6
- const validateLxmParam = (value: string) => value === '*' || isNSID(value)
7
- const validateAudParam = (value: string) => value === '*' || isDIDLike(value)
8
-
9
- export const rpcParser = new Parser(
10
- 'rpc',
11
- {
12
- lxm: {
13
- multiple: true,
14
- required: true,
15
- validate: validateLxmParam,
16
- },
17
- aud: {
18
- multiple: false,
19
- required: true,
20
- validate: validateAudParam,
21
- },
22
- },
23
- 'lxm',
24
- )
25
-
26
- export type RpcScopeMatch = {
27
- lxm: string
28
- aud: string
29
- }
30
-
31
- export class RpcScope {
32
- constructor(
33
- public readonly aud: '*' | DIDLike,
34
- public readonly lxm: NeRoArray<'*' | NSID>,
35
- ) {}
36
-
37
- matches(options: RpcScopeMatch): boolean {
38
- const { aud, lxm } = this
39
- return (
40
- (aud === '*' || aud === options.aud) &&
41
- (lxm.includes('*') || (lxm as readonly string[]).includes(options.lxm))
42
- )
43
- }
44
-
45
- toString(): string {
46
- const { lxm, aud } = this
47
- return rpcParser.format({
48
- aud,
49
- lxm: lxm.includes('*')
50
- ? ['*']
51
- : ([...new Set(lxm)].sort() as [NSID, ...NSID[]]),
52
- })
53
- }
54
-
55
- static fromString(scope: string): RpcScope | null {
56
- if (!isScopeForResource(scope, 'rpc')) return null
57
- const syntax = ResourceSyntax.fromString(scope)
58
- return this.fromSyntax(syntax)
59
- }
60
-
61
- static fromSyntax(syntax: ResourceSyntax): RpcScope | null {
62
- const result = rpcParser.parse(syntax)
63
- if (!result) return null
64
-
65
- // rpc:*?aud=* is forbidden
66
- if (result.aud === '*' && result.lxm.includes('*')) return null
67
-
68
- return new RpcScope(result.aud, result.lxm)
69
- }
70
-
71
- static scopeNeededFor(options: RpcScopeMatch): string {
72
- return rpcParser.format({
73
- aud: options.aud as DIDLike,
74
- lxm: [options.lxm as NSID],
75
- })
76
- }
77
- }
@@ -1,203 +0,0 @@
1
- import { ResourceSyntax, isScopeForResource } from './syntax.js'
2
-
3
- describe('isScopeForResource', () => {
4
- describe('exact match', () => {
5
- it('should return true for exact match', () => {
6
- expect(isScopeForResource('resource', 'resource')).toBe(true)
7
- })
8
-
9
- it('should return false for different resource', () => {
10
- expect(isScopeForResource('resource', 'differentResource')).toBe(false)
11
- })
12
- })
13
-
14
- describe('with positional parameter', () => {
15
- it('should return true for exact match with positional parameter', () => {
16
- expect(isScopeForResource('resource:positional', 'resource')).toBe(true)
17
- })
18
-
19
- it('should return false for different resource with positional parameter', () => {
20
- expect(
21
- isScopeForResource('differentResource:positional', 'resource'),
22
- ).toBe(false)
23
- })
24
- })
25
-
26
- describe('with named parameters', () => {
27
- it('should return true for exact match with named parameters', () => {
28
- expect(isScopeForResource('resource?param=value', 'resource')).toBe(true)
29
- })
30
-
31
- it('should return false for different resource with named parameters', () => {
32
- expect(
33
- isScopeForResource('differentResource?param=value', 'resource'),
34
- ).toBe(false)
35
- })
36
- })
37
- })
38
-
39
- for (const { scope, normalized = scope, content } of [
40
- {
41
- scope: 'my-res',
42
- content: { resource: 'my-res' },
43
- },
44
- {
45
- scope: 'my-res:my-pos',
46
- content: { resource: 'my-res', positional: 'my-pos' },
47
- },
48
- {
49
- scope: 'my-res:',
50
- content: { resource: 'my-res', positional: '' },
51
- },
52
- {
53
- scope: 'my-res:foo?x=value&y=value-y',
54
- content: {
55
- resource: 'my-res',
56
- positional: 'foo',
57
- params: { x: ['value'], y: ['value-y'] },
58
- },
59
- },
60
- {
61
- scope: 'my-res?x=value&y=value-y',
62
- content: { resource: 'my-res', params: { x: ['value'], y: ['value-y'] } },
63
- },
64
- {
65
- scope: 'my-res?x=foo&x=bar&x=baz',
66
- content: { resource: 'my-res', params: { x: ['foo', 'bar', 'baz'] } },
67
- },
68
- {
69
- scope: 'rpc:foo.bar?aud=did:foo:bar?lxm=bar.baz',
70
- normalized: 'rpc:foo.bar?aud=did:foo:bar%3Flxm%3Dbar.baz',
71
- content: {
72
- resource: 'rpc',
73
- positional: 'foo.bar',
74
- params: { aud: ['did:foo:bar?lxm=bar.baz'] },
75
- },
76
- },
77
- ] as Array<{
78
- scope: string
79
- normalized?: string
80
- content: {
81
- resource: string
82
- positional?: string
83
- params?: Record<string, string[]>
84
- }
85
- }>) {
86
- describe(`Valid "${scope}"`, () => {
87
- const syntax = ResourceSyntax.fromString(scope)
88
-
89
- it('should match the expected syntax', () => {
90
- expect(syntax).toEqual({
91
- resource: content.resource,
92
- positional: content.positional,
93
- params: content.params
94
- ? new URLSearchParams(
95
- Object.entries(content.params).flatMap(([k, v]) =>
96
- v.map((val) => [k, val]),
97
- ),
98
- )
99
- : undefined,
100
- })
101
- })
102
-
103
- it(`should stringify ${scope} correctly`, () => {
104
- expect(syntax.toString()).toBe(normalized)
105
- })
106
-
107
- it(`should parse ${scope} correctly`, () => {
108
- expect(syntax.toJSON()).toMatchObject(content)
109
- })
110
-
111
- it(`should match ${scope} resource`, () => {
112
- expect(syntax.is(content.resource)).toBe(true)
113
- })
114
-
115
- it(`should return undefined for nonexistent single-value param`, () => {
116
- expect(syntax.getSingle('nonexistent')).toBeUndefined()
117
- })
118
-
119
- it(`should return undefined for nonexistent multi-value param`, () => {
120
- expect(syntax.getMulti('nonexistent')).toBeUndefined()
121
- })
122
-
123
- const { params } = content
124
- if (params) {
125
- it(`should allow detecting unknown params`, () => {
126
- const allowedParams = Object.keys(params) as [string, ...string[]]
127
- expect(syntax.containsParamsOtherThan(allowedParams)).toBe(false)
128
-
129
- if (allowedParams.length > 1) {
130
- const woFirst = allowedParams.slice(1) as [string, ...string[]]
131
- expect(syntax.containsParamsOtherThan(woFirst)).toBe(true)
132
-
133
- const woLast = allowedParams.slice(0, -1) as [string, ...string[]]
134
- expect(syntax.containsParamsOtherThan(woLast)).toBe(true)
135
- }
136
- })
137
-
138
- for (const [key, values] of Object.entries(params)) {
139
- it(`should get an array when reading "${key}"`, () => {
140
- expect(syntax.getMulti(key)).toEqual(values)
141
- })
142
-
143
- if (values.length === 1) {
144
- it(`should allow retrieving single-value params`, () => {
145
- expect(syntax.getSingle(key)).toEqual(values[0])
146
- })
147
- } else {
148
- it(`should return null for multi-value params`, () => {
149
- expect(syntax.getSingle(key)).toBeNull()
150
- expect(syntax.getSingle(key, true)).toBeNull()
151
- })
152
- }
153
- }
154
- }
155
-
156
- const { positional } = content
157
- if (positional !== undefined) {
158
- it(`should return positional parameter`, () => {
159
- expect(syntax.positional).toBe(positional)
160
- })
161
-
162
- it(`should return positional parameter when reading as single-value`, () => {
163
- expect(syntax.getSingle('nonexistent', true)).toBe(positional)
164
- })
165
-
166
- it(`should return positional parameter when reading as multi-value`, () => {
167
- expect(syntax.getMulti('nonexistent', true)).toEqual([positional])
168
- })
169
- }
170
- })
171
- }
172
-
173
- describe('invalid positional parameters', () => {
174
- it('should return null for positional parameters used together with named parameters', () => {
175
- const syntax = ResourceSyntax.fromString('my-res:pos?x=value')
176
- expect(syntax.getSingle('x', true)).toBeNull()
177
- expect(syntax.getMulti('x', true)).toBeNull()
178
- })
179
- })
180
-
181
- describe('containsParamsOtherThan', () => {
182
- it('should return true if there are additional params', () => {
183
- const syntax = ResourceSyntax.fromString('my-res?x=value&y=value-y')
184
- expect(syntax.containsParamsOtherThan(['x'])).toBe(true)
185
- })
186
- })
187
-
188
- describe('url encoding', () => {
189
- it('should handle URL encoding in positional parameters', () => {
190
- const syntax = ResourceSyntax.fromString('my-res:my%20pos')
191
- expect(syntax.positional).toBe('my pos')
192
- })
193
-
194
- it('should handle URL encoding in named parameters', () => {
195
- const syntax = ResourceSyntax.fromString('my-res?x=my%20value')
196
- expect(syntax.getSingle('x')).toBe('my value')
197
- })
198
-
199
- it(`should allow colon (:) in positional parameters`, () => {
200
- const syntax = ResourceSyntax.fromString('my-res:my:pos')
201
- expect(syntax.positional).toBe('my:pos')
202
- })
203
- })