@atproto/oauth-scopes 0.0.1
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 +7 -0
- package/LICENSE.txt +7 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/did.d.ts +3 -0
- package/dist/lib/did.d.ts.map +1 -0
- package/dist/lib/did.js +6 -0
- package/dist/lib/did.js.map +1 -0
- package/dist/lib/mime.d.ts +7 -0
- package/dist/lib/mime.d.ts.map +1 -0
- package/dist/lib/mime.js +65 -0
- package/dist/lib/mime.js.map +1 -0
- package/dist/lib/nsid.d.ts +3 -0
- package/dist/lib/nsid.d.ts.map +1 -0
- package/dist/lib/nsid.js +9 -0
- package/dist/lib/nsid.js.map +1 -0
- package/dist/lib/util.d.ts +3 -0
- package/dist/lib/util.d.ts.map +1 -0
- package/dist/lib/util.js +24 -0
- package/dist/lib/util.js.map +1 -0
- package/dist/parser.d.ts +31 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +118 -0
- package/dist/parser.js.map +1 -0
- package/dist/permission-set-transition.d.ts +15 -0
- package/dist/permission-set-transition.d.ts.map +1 -0
- package/dist/permission-set-transition.js +52 -0
- package/dist/permission-set-transition.js.map +1 -0
- package/dist/permission-set.d.ts +22 -0
- package/dist/permission-set.d.ts.map +1 -0
- package/dist/permission-set.js +68 -0
- package/dist/permission-set.js.map +1 -0
- package/dist/resources/account-scope.d.ts +35 -0
- package/dist/resources/account-scope.d.ts.map +1 -0
- package/dist/resources/account-scope.js +60 -0
- package/dist/resources/account-scope.js.map +1 -0
- package/dist/resources/blob-scope.d.ts +25 -0
- package/dist/resources/blob-scope.d.ts.map +1 -0
- package/dist/resources/blob-scope.js +74 -0
- package/dist/resources/blob-scope.js.map +1 -0
- package/dist/resources/identity-scope.d.ts +25 -0
- package/dist/resources/identity-scope.d.ts.map +1 -0
- package/dist/resources/identity-scope.js +46 -0
- package/dist/resources/identity-scope.js.map +1 -0
- package/dist/resources/repo-scope.d.ts +37 -0
- package/dist/resources/repo-scope.d.ts.map +1 -0
- package/dist/resources/repo-scope.js +92 -0
- package/dist/resources/repo-scope.js.map +1 -0
- package/dist/resources/rpc-scope.d.ts +31 -0
- package/dist/resources/rpc-scope.d.ts.map +1 -0
- package/dist/resources/rpc-scope.js +74 -0
- package/dist/resources/rpc-scope.js.map +1 -0
- package/dist/scope-missing-error.d.ts +9 -0
- package/dist/scope-missing-error.d.ts.map +1 -0
- package/dist/scope-missing-error.js +39 -0
- package/dist/scope-missing-error.js.map +1 -0
- package/dist/scopes-set.d.ts +21 -0
- package/dist/scopes-set.d.ts.map +1 -0
- package/dist/scopes-set.js +55 -0
- package/dist/scopes-set.js.map +1 -0
- package/dist/syntax.d.ts +76 -0
- package/dist/syntax.d.ts.map +1 -0
- package/dist/syntax.js +249 -0
- package/dist/syntax.js.map +1 -0
- package/dist/utilities.d.ts +17 -0
- package/dist/utilities.d.ts.map +1 -0
- package/dist/utilities.js +108 -0
- package/dist/utilities.js.map +1 -0
- package/jest.config.js +5 -0
- package/package.json +36 -0
- package/src/index.ts +17 -0
- package/src/lib/did.ts +3 -0
- package/src/lib/mime.test.ts +98 -0
- package/src/lib/mime.ts +70 -0
- package/src/lib/nsid.ts +6 -0
- package/src/lib/util.ts +19 -0
- package/src/parser.ts +150 -0
- package/src/permission-set-transition.test.ts +109 -0
- package/src/permission-set-transition.ts +67 -0
- package/src/permission-set.test.ts +225 -0
- package/src/permission-set.ts +78 -0
- package/src/resources/account-scope.test.ts +175 -0
- package/src/resources/account-scope.ts +66 -0
- package/src/resources/blob-scope.test.ts +118 -0
- package/src/resources/blob-scope.ts +86 -0
- package/src/resources/identity-scope.test.ts +80 -0
- package/src/resources/identity-scope.ts +49 -0
- package/src/resources/repo-scope.test.ts +255 -0
- package/src/resources/repo-scope.ts +101 -0
- package/src/resources/rpc-scope.test.ts +280 -0
- package/src/resources/rpc-scope.ts +77 -0
- package/src/scope-missing-error.ts +15 -0
- package/src/scopes-set.test.ts +47 -0
- package/src/scopes-set.ts +60 -0
- package/src/syntax.test.ts +203 -0
- package/src/syntax.ts +325 -0
- package/src/utilities.ts +109 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +7 -0
- package/tsconfig.tests.json +7 -0
- package/tsconfig.tests.tsbuildinfo +1 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { RepoScope } from './repo-scope.js'
|
|
2
|
+
|
|
3
|
+
describe('RepoScope', () => {
|
|
4
|
+
describe('static', () => {
|
|
5
|
+
describe('fromString', () => {
|
|
6
|
+
it('should parse positional scope', () => {
|
|
7
|
+
const scope = RepoScope.fromString('repo:foo.bar')
|
|
8
|
+
expect(scope).not.toBeNull()
|
|
9
|
+
expect(scope!.collection).toEqual(['foo.bar'])
|
|
10
|
+
expect(scope!.action).toEqual(['create', 'update', 'delete'])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should parse valid repo scope with multiple actions', () => {
|
|
14
|
+
const scope = RepoScope.fromString(
|
|
15
|
+
'repo:foo.bar?action=create&action=update',
|
|
16
|
+
)
|
|
17
|
+
expect(scope).not.toBeNull()
|
|
18
|
+
expect(scope!.collection).toEqual(['foo.bar'])
|
|
19
|
+
expect(scope!.action).toEqual(['create', 'update'])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should parse valid repo scope without actions (defaults to create, update, delete)', () => {
|
|
23
|
+
const scope = RepoScope.fromString('repo:foo.bar')
|
|
24
|
+
expect(scope).not.toBeNull()
|
|
25
|
+
expect(scope!.collection).toEqual(['foo.bar'])
|
|
26
|
+
expect(scope!.action).toEqual(['create', 'update', 'delete'])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should allow wildcard collection with specific action', () => {
|
|
30
|
+
const scope = RepoScope.fromString('repo:*?action=create')
|
|
31
|
+
expect(scope).not.toBeNull()
|
|
32
|
+
expect(scope!.collection).toEqual(['*'])
|
|
33
|
+
expect(scope!.action).toEqual(['create'])
|
|
34
|
+
expect(
|
|
35
|
+
scope!.matches({ action: 'create', collection: 'any.collection' }),
|
|
36
|
+
).toBe(true)
|
|
37
|
+
expect(
|
|
38
|
+
scope!.matches({ action: 'update', collection: 'any.collection' }),
|
|
39
|
+
).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should allow wildcard collection without actions', () => {
|
|
43
|
+
const scope = RepoScope.fromString('repo:*')
|
|
44
|
+
expect(scope).not.toBeNull()
|
|
45
|
+
expect(scope!.collection).toEqual(['*'])
|
|
46
|
+
expect(scope!.action).toEqual(['create', 'update', 'delete'])
|
|
47
|
+
expect(
|
|
48
|
+
scope!.matches({ action: 'create', collection: 'any.collection' }),
|
|
49
|
+
).toBe(true)
|
|
50
|
+
expect(
|
|
51
|
+
scope!.matches({ action: 'update', collection: 'any.collection' }),
|
|
52
|
+
).toBe(true)
|
|
53
|
+
expect(
|
|
54
|
+
scope!.matches({ action: 'delete', collection: 'any.collection' }),
|
|
55
|
+
).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should ignore scopes with invalid collection names', () => {
|
|
59
|
+
expect(RepoScope.fromString('repo:foo bar')).toBeNull()
|
|
60
|
+
expect(RepoScope.fromString('repo:.foo')).toBeNull()
|
|
61
|
+
expect(RepoScope.fromString('repo:bar.')).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should reject invalid action names', () => {
|
|
65
|
+
const scope = RepoScope.fromString('repo:foo.bar?action=invalid')
|
|
66
|
+
expect(scope).toBeNull()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should return null for invalid repo scope', () => {
|
|
70
|
+
expect(RepoScope.fromString('invalid')).toBeNull()
|
|
71
|
+
expect(RepoScope.fromString('scope')).toBeNull()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
for (const invalid of [
|
|
75
|
+
'repo:*?action=*',
|
|
76
|
+
'invalid',
|
|
77
|
+
'repo:invalid',
|
|
78
|
+
'repo:foo.bar?action=invalid',
|
|
79
|
+
'repo?collection=invalid&action=invalid',
|
|
80
|
+
]) {
|
|
81
|
+
it(`should return null for invalid rpc scope: ${invalid}`, () => {
|
|
82
|
+
expect(RepoScope.fromString(invalid)).toBeNull()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('scopeNeededFor', () => {
|
|
88
|
+
it('should return correct scope string for specific collection and action', () => {
|
|
89
|
+
const scope = RepoScope.scopeNeededFor({
|
|
90
|
+
collection: 'foo.bar',
|
|
91
|
+
action: 'create',
|
|
92
|
+
})
|
|
93
|
+
expect(scope).toBe('repo:foo.bar?action=create')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should return scope that accepts all collections with specific action', () => {
|
|
97
|
+
const scope = RepoScope.scopeNeededFor({
|
|
98
|
+
collection: '*',
|
|
99
|
+
action: 'create',
|
|
100
|
+
})
|
|
101
|
+
expect(scope).toBe('repo:*?action=create')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('ignores invalid collection names', () => {
|
|
105
|
+
// @NOTE the scopeNeededFor assumes valid input, so it does not validate
|
|
106
|
+
// collection or action.
|
|
107
|
+
const scope = RepoScope.scopeNeededFor({
|
|
108
|
+
collection: 'invalid',
|
|
109
|
+
// @ts-expect-error
|
|
110
|
+
action: 'not-an-action',
|
|
111
|
+
})
|
|
112
|
+
expect(scope).toBe('repo:invalid?action=not-an-action')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('instance', () => {
|
|
118
|
+
describe('matches', () => {
|
|
119
|
+
it('should match create action', () => {
|
|
120
|
+
const scope = RepoScope.fromString('repo:foo.bar?action=create')
|
|
121
|
+
expect(scope).not.toBeNull()
|
|
122
|
+
expect(
|
|
123
|
+
scope!.matches({ action: 'create', collection: 'foo.bar' }),
|
|
124
|
+
).toBe(true)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should not match unspecified action', () => {
|
|
128
|
+
const scope = RepoScope.fromString('repo:foo.bar?action=create')
|
|
129
|
+
expect(scope).not.toBeNull()
|
|
130
|
+
expect(
|
|
131
|
+
scope!.matches({ action: 'update', collection: 'foo.bar' }),
|
|
132
|
+
).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should match wildcard collection', () => {
|
|
136
|
+
const scope = RepoScope.fromString('repo:*?action=create')
|
|
137
|
+
expect(scope).not.toBeNull()
|
|
138
|
+
expect(
|
|
139
|
+
scope!.matches({ action: 'create', collection: 'any.collection' }),
|
|
140
|
+
).toBe(true)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should not match different action with wildcard collection', () => {
|
|
144
|
+
const scope = RepoScope.fromString('repo:*?action=create')
|
|
145
|
+
expect(scope).not.toBeNull()
|
|
146
|
+
expect(
|
|
147
|
+
scope!.matches({ action: 'delete', collection: 'any.collection' }),
|
|
148
|
+
).toBe(false)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should match multiple actions', () => {
|
|
152
|
+
const scope = RepoScope.fromString(
|
|
153
|
+
'repo:foo.bar?action=create&action=update',
|
|
154
|
+
)
|
|
155
|
+
expect(scope).not.toBeNull()
|
|
156
|
+
expect(
|
|
157
|
+
scope!.matches({ action: 'create', collection: 'foo.bar' }),
|
|
158
|
+
).toBe(true)
|
|
159
|
+
expect(
|
|
160
|
+
scope!.matches({ action: 'update', collection: 'foo.bar' }),
|
|
161
|
+
).toBe(true)
|
|
162
|
+
expect(
|
|
163
|
+
scope!.matches({ action: 'delete', collection: 'foo.bar' }),
|
|
164
|
+
).toBe(false)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should default to "create", "update", and "delete" actions', () => {
|
|
168
|
+
const scope = RepoScope.fromString('repo:foo.bar')
|
|
169
|
+
expect(scope).not.toBeNull()
|
|
170
|
+
expect(
|
|
171
|
+
scope!.matches({ action: 'create', collection: 'foo.bar' }),
|
|
172
|
+
).toBe(true)
|
|
173
|
+
expect(
|
|
174
|
+
scope!.matches({ action: 'update', collection: 'foo.bar' }),
|
|
175
|
+
).toBe(true)
|
|
176
|
+
expect(
|
|
177
|
+
scope!.matches({ action: 'delete', collection: 'foo.bar' }),
|
|
178
|
+
).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('toString', () => {
|
|
183
|
+
it('should format repo scope correctly', () => {
|
|
184
|
+
const scope = new RepoScope(['foo.bar'], ['create', 'update'])
|
|
185
|
+
expect(scope).not.toBeNull()
|
|
186
|
+
expect(scope!.toString()).toBe(
|
|
187
|
+
'repo:foo.bar?action=create&action=update',
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('consistency', () => {
|
|
194
|
+
const testCases: { input: string; expected: string }[] = [
|
|
195
|
+
{ input: 'repo:foo.bar', expected: 'repo:foo.bar' },
|
|
196
|
+
{
|
|
197
|
+
input: 'repo:foo.bar?action=create',
|
|
198
|
+
expected: 'repo:foo.bar?action=create',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
input: 'repo:foo.bar?action=create&action=update',
|
|
202
|
+
expected: 'repo:foo.bar?action=create&action=update',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
input: 'repo:*?action=create&action=update&action=delete',
|
|
206
|
+
expected: 'repo:*',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
input: 'repo:foo.bar?action=create&action=update&action=delete',
|
|
210
|
+
expected: 'repo:foo.bar',
|
|
211
|
+
},
|
|
212
|
+
{ input: 'repo:*?action=create', expected: 'repo:*?action=create' },
|
|
213
|
+
{ input: 'repo:*?action=update', expected: 'repo:*?action=update' },
|
|
214
|
+
{
|
|
215
|
+
input: 'repo?collection=*&action=update',
|
|
216
|
+
expected: 'repo:*?action=update',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
input: 'repo?collection=*&collection=foo.bar&action=update',
|
|
220
|
+
expected: 'repo:*?action=update',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
input: 'repo?collection=*',
|
|
224
|
+
expected: 'repo:*',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
input: 'repo?collection=*&action=create&action=update&action=delete',
|
|
228
|
+
expected: 'repo:*',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
input: 'repo?collection=*&collection=foo.bar',
|
|
232
|
+
expected: 'repo:*',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
input: 'repo?action=create&collection=foo.bar',
|
|
236
|
+
expected: 'repo:foo.bar?action=create',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
input:
|
|
240
|
+
'repo?collection=foo.bar&action=create&action=update&action=delete',
|
|
241
|
+
expected: 'repo:foo.bar',
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
input: 'repo?action=create&collection=foo.bar&collection=baz.qux',
|
|
245
|
+
expected: 'repo?collection=baz.qux&collection=foo.bar&action=create',
|
|
246
|
+
},
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
for (const { input, expected } of testCases) {
|
|
250
|
+
it(`should properly re-format ${input}`, () => {
|
|
251
|
+
expect(RepoScope.fromString(input)?.toString()).toBe(expected)
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NSID, isNSID } from '../lib/nsid.js'
|
|
2
|
+
import { Parser, knownValuesValidator } from '../parser.js'
|
|
3
|
+
import { NeRoArray, ResourceSyntax, isScopeForResource } from '../syntax.js'
|
|
4
|
+
|
|
5
|
+
const REPO_ACTIONS = Object.freeze(['create', 'update', 'delete'] as const)
|
|
6
|
+
export type RepoAction = (typeof REPO_ACTIONS)[number]
|
|
7
|
+
export const isRepoAction = knownValuesValidator(REPO_ACTIONS)
|
|
8
|
+
|
|
9
|
+
export const repoParser = new Parser(
|
|
10
|
+
'repo',
|
|
11
|
+
{
|
|
12
|
+
collection: {
|
|
13
|
+
multiple: true,
|
|
14
|
+
required: true,
|
|
15
|
+
validate: (value) => value === '*' || isNSID(value),
|
|
16
|
+
normalize: (value) => {
|
|
17
|
+
if (value.length > 1 && value.includes('*')) return ['*'] as const
|
|
18
|
+
return value
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
action: {
|
|
22
|
+
multiple: true,
|
|
23
|
+
required: false,
|
|
24
|
+
validate: isRepoAction,
|
|
25
|
+
default: REPO_ACTIONS,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
'collection',
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export type RepoScopeMatch = {
|
|
32
|
+
collection: string
|
|
33
|
+
action: RepoAction
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class RepoScope {
|
|
37
|
+
constructor(
|
|
38
|
+
public readonly collection: NeRoArray<'*' | NSID>,
|
|
39
|
+
public readonly action: NeRoArray<RepoAction>,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
get allowsAnyCollection() {
|
|
43
|
+
return this.collection.includes('*')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
matches({ action, collection }: RepoScopeMatch): boolean {
|
|
47
|
+
return (
|
|
48
|
+
this.action.includes(action) &&
|
|
49
|
+
(this.allowsAnyCollection ||
|
|
50
|
+
(this.collection as readonly string[]).includes(collection))
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toString(): string {
|
|
55
|
+
// Normalize (compress, de-dupe, sort)
|
|
56
|
+
return repoParser.format({
|
|
57
|
+
collection: this.allowsAnyCollection
|
|
58
|
+
? ['*']
|
|
59
|
+
: this.collection.length > 1
|
|
60
|
+
? ([...new Set(this.collection)].sort() as [NSID, ...NSID[]])
|
|
61
|
+
: this.collection,
|
|
62
|
+
action:
|
|
63
|
+
this.action === REPO_ACTIONS
|
|
64
|
+
? REPO_ACTIONS // No need to filter if the default was used
|
|
65
|
+
: (REPO_ACTIONS.filter(includedIn, this.action) as [
|
|
66
|
+
RepoAction,
|
|
67
|
+
...RepoAction[],
|
|
68
|
+
]),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static fromString(scope: string): RepoScope | null {
|
|
73
|
+
if (!isScopeForResource(scope, 'repo')) return null
|
|
74
|
+
const syntax = ResourceSyntax.fromString(scope)
|
|
75
|
+
return this.fromSyntax(syntax)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static fromSyntax(syntax: ResourceSyntax): RepoScope | null {
|
|
79
|
+
const result = repoParser.parse(syntax)
|
|
80
|
+
if (!result) return null
|
|
81
|
+
|
|
82
|
+
return new RepoScope(result.collection, result.action)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static scopeNeededFor(options: RepoScopeMatch): string {
|
|
86
|
+
return repoParser.format({
|
|
87
|
+
collection: [options.collection as '*' | NSID],
|
|
88
|
+
action: [options.action],
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Special utility function to be used as predicate for array methods like
|
|
95
|
+
* `Array.prototype.includes`, etc. When used as predicate, it expects that
|
|
96
|
+
* the array method is called with a `thisArg` that is a readonly array of
|
|
97
|
+
* the same type as the `value` parameter.
|
|
98
|
+
*/
|
|
99
|
+
function includedIn<T>(this: readonly T[], value: T): boolean {
|
|
100
|
+
return this.includes(value)
|
|
101
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
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
|
+
})
|