@atproto/oauth-scopes 0.0.2 → 0.2.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.
- package/CHANGELOG.md +38 -0
- package/dist/atproto-oauth-scope.d.ts +17 -0
- package/dist/atproto-oauth-scope.d.ts.map +1 -0
- package/dist/atproto-oauth-scope.js +67 -0
- package/dist/atproto-oauth-scope.js.map +1 -0
- package/dist/index.d.ts +9 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -13
- package/dist/index.js.map +1 -1
- package/dist/lib/lexicon.d.ts +2 -0
- package/dist/lib/lexicon.d.ts.map +1 -0
- package/dist/lib/lexicon.js +3 -0
- package/dist/lib/lexicon.js.map +1 -0
- package/dist/lib/mime.d.ts +1 -1
- package/dist/lib/mime.d.ts.map +1 -1
- package/dist/lib/mime.js +2 -0
- package/dist/lib/mime.js.map +1 -1
- package/dist/lib/nsid.d.ts +2 -2
- package/dist/lib/nsid.d.ts.map +1 -1
- package/dist/lib/nsid.js +4 -6
- package/dist/lib/nsid.js.map +1 -1
- package/dist/lib/parser.d.ts +29 -0
- package/dist/lib/parser.d.ts.map +1 -0
- package/dist/lib/parser.js +152 -0
- package/dist/lib/parser.js.map +1 -0
- package/dist/lib/resource-permission.d.ts +10 -0
- package/dist/lib/resource-permission.d.ts.map +1 -0
- package/dist/lib/resource-permission.js +3 -0
- package/dist/lib/resource-permission.js.map +1 -0
- package/dist/lib/syntax-lexicon.d.ts +26 -0
- package/dist/lib/syntax-lexicon.d.ts.map +1 -0
- package/dist/lib/syntax-lexicon.js +58 -0
- package/dist/lib/syntax-lexicon.js.map +1 -0
- package/dist/lib/syntax-string.d.ts +16 -0
- package/dist/lib/syntax-string.d.ts.map +1 -0
- package/dist/lib/syntax-string.js +121 -0
- package/dist/lib/syntax-string.js.map +1 -0
- package/dist/lib/syntax.d.ts +23 -0
- package/dist/lib/syntax.d.ts.map +1 -0
- package/dist/lib/syntax.js +22 -0
- package/dist/lib/syntax.js.map +1 -0
- package/dist/lib/util.d.ts +5 -1
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +8 -12
- package/dist/lib/util.js.map +1 -1
- package/dist/scope-permissions-transition.d.ts +15 -0
- package/dist/scope-permissions-transition.d.ts.map +1 -0
- package/dist/{permission-set-transition.js → scope-permissions-transition.js} +5 -5
- package/dist/scope-permissions-transition.js.map +1 -0
- package/dist/scope-permissions.d.ts +22 -0
- package/dist/scope-permissions.d.ts.map +1 -0
- package/dist/{permission-set.js → scope-permissions.js} +20 -16
- package/dist/scope-permissions.js.map +1 -0
- package/dist/scopes/account-permission.d.ts +35 -0
- package/dist/scopes/account-permission.d.ts.map +1 -0
- package/dist/scopes/account-permission.js +71 -0
- package/dist/scopes/account-permission.js.map +1 -0
- package/dist/scopes/blob-permission.d.ts +27 -0
- package/dist/scopes/blob-permission.d.ts.map +1 -0
- package/dist/scopes/blob-permission.js +86 -0
- package/dist/scopes/blob-permission.js.map +1 -0
- package/dist/scopes/identity-permission.d.ts +25 -0
- package/dist/scopes/identity-permission.d.ts.map +1 -0
- package/dist/scopes/identity-permission.js +53 -0
- package/dist/scopes/identity-permission.js.map +1 -0
- package/dist/scopes/include-scope.d.ts +54 -0
- package/dist/scopes/include-scope.d.ts.map +1 -0
- package/dist/scopes/include-scope.js +156 -0
- package/dist/scopes/include-scope.js.map +1 -0
- package/dist/scopes/repo-permission.d.ts +40 -0
- package/dist/scopes/repo-permission.d.ts.map +1 -0
- package/dist/scopes/repo-permission.js +101 -0
- package/dist/scopes/repo-permission.js.map +1 -0
- package/dist/scopes/rpc-permission.d.ts +38 -0
- package/dist/scopes/rpc-permission.d.ts.map +1 -0
- package/dist/scopes/rpc-permission.js +81 -0
- package/dist/scopes/rpc-permission.js.map +1 -0
- package/dist/scopes-set.d.ts +12 -1
- package/dist/scopes-set.d.ts.map +1 -1
- package/dist/scopes-set.js +49 -3
- package/dist/scopes-set.js.map +1 -1
- package/package.json +7 -3
- package/src/atproto-oauth-scope.ts +79 -0
- package/src/index.ts +10 -14
- package/src/lib/lexicon.ts +1 -0
- package/src/lib/mime.ts +2 -1
- package/src/lib/nsid.ts +5 -6
- package/src/lib/parser.ts +176 -0
- package/src/lib/resource-permission.ts +10 -0
- package/src/lib/syntax-lexicon.ts +55 -0
- package/src/lib/syntax-string.test.ts +130 -0
- package/src/lib/syntax-string.ts +132 -0
- package/src/lib/syntax.test.ts +43 -0
- package/src/lib/syntax.ts +47 -0
- package/src/lib/util.ts +11 -12
- package/src/{permission-set-transition.test.ts → scope-permissions-transition.test.ts} +33 -20
- package/src/{permission-set-transition.ts → scope-permissions-transition.ts} +11 -11
- package/src/{permission-set.test.ts → scope-permissions.test.ts} +77 -35
- package/src/scope-permissions.ts +91 -0
- package/src/{resources/account-scope.test.ts → scopes/account-permission.test.ts} +45 -33
- package/src/scopes/account-permission.ts +75 -0
- package/src/{resources/blob-scope.test.ts → scopes/blob-permission.test.ts} +31 -23
- package/src/scopes/blob-permission.ts +105 -0
- package/src/{resources/identity-scope.test.ts → scopes/identity-permission.test.ts} +13 -13
- package/src/scopes/identity-permission.ts +54 -0
- package/src/scopes/include-scope.test.ts +626 -0
- package/src/scopes/include-scope.ts +168 -0
- package/src/{resources/repo-scope.test.ts → scopes/repo-permission.test.ts} +77 -65
- package/src/scopes/repo-permission.ts +111 -0
- package/src/scopes/rpc-permission.test.ts +323 -0
- package/src/scopes/rpc-permission.ts +85 -0
- package/src/scopes-set.test.ts +5 -5
- package/src/scopes-set.ts +79 -5
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
- package/dist/lib/did.d.ts +0 -3
- package/dist/lib/did.d.ts.map +0 -1
- package/dist/lib/did.js +0 -6
- package/dist/lib/did.js.map +0 -1
- package/dist/parser.d.ts +0 -31
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js +0 -118
- package/dist/parser.js.map +0 -1
- package/dist/permission-set-transition.d.ts +0 -15
- package/dist/permission-set-transition.d.ts.map +0 -1
- package/dist/permission-set-transition.js.map +0 -1
- package/dist/permission-set.d.ts +0 -22
- package/dist/permission-set.d.ts.map +0 -1
- package/dist/permission-set.js.map +0 -1
- package/dist/resources/account-scope.d.ts +0 -35
- package/dist/resources/account-scope.d.ts.map +0 -1
- package/dist/resources/account-scope.js +0 -60
- package/dist/resources/account-scope.js.map +0 -1
- package/dist/resources/blob-scope.d.ts +0 -25
- package/dist/resources/blob-scope.d.ts.map +0 -1
- package/dist/resources/blob-scope.js +0 -74
- package/dist/resources/blob-scope.js.map +0 -1
- package/dist/resources/identity-scope.d.ts +0 -25
- package/dist/resources/identity-scope.d.ts.map +0 -1
- package/dist/resources/identity-scope.js +0 -46
- package/dist/resources/identity-scope.js.map +0 -1
- package/dist/resources/repo-scope.d.ts +0 -37
- package/dist/resources/repo-scope.d.ts.map +0 -1
- package/dist/resources/repo-scope.js +0 -92
- package/dist/resources/repo-scope.js.map +0 -1
- package/dist/resources/rpc-scope.d.ts +0 -31
- package/dist/resources/rpc-scope.d.ts.map +0 -1
- package/dist/resources/rpc-scope.js +0 -74
- package/dist/resources/rpc-scope.js.map +0 -1
- package/dist/syntax.d.ts +0 -76
- package/dist/syntax.d.ts.map +0 -1
- package/dist/syntax.js +0 -249
- package/dist/syntax.js.map +0 -1
- package/dist/utilities.d.ts +0 -17
- package/dist/utilities.d.ts.map +0 -1
- package/dist/utilities.js +0 -108
- package/dist/utilities.js.map +0 -1
- package/src/lib/did.ts +0 -3
- package/src/parser.ts +0 -150
- package/src/permission-set.ts +0 -78
- package/src/resources/account-scope.ts +0 -66
- package/src/resources/blob-scope.ts +0 -86
- package/src/resources/identity-scope.ts +0 -49
- package/src/resources/repo-scope.ts +0 -101
- package/src/resources/rpc-scope.test.ts +0 -280
- package/src/resources/rpc-scope.ts +0 -77
- package/src/syntax.test.ts +0 -203
- package/src/syntax.ts +0 -325
- 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
|
+
}
|
package/src/scopes-set.test.ts
CHANGED
|
@@ -22,19 +22,19 @@ describe('ScopesSet', () => {
|
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
it('should match included scopes', () => {
|
|
25
|
-
const set = new ScopesSet(['repo:foo
|
|
25
|
+
const set = new ScopesSet(['repo:com.example.foo'])
|
|
26
26
|
expect(
|
|
27
|
-
set.matches('repo', { action: 'create', collection: 'foo
|
|
27
|
+
set.matches('repo', { action: 'create', collection: 'com.example.foo' }),
|
|
28
28
|
).toBe(true)
|
|
29
29
|
expect(
|
|
30
|
-
set.matches('repo', { action: 'create', collection: '
|
|
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
|
|
35
|
+
const set = new ScopesSet(['repo:com.example.foo?action=create'])
|
|
36
36
|
expect(
|
|
37
|
-
set.matches('repo', { action: 'delete', collection: 'foo
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 (
|
|
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/
|
|
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/
|
|
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
package/dist/lib/did.d.ts.map
DELETED
|
@@ -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
package/dist/lib/did.js.map
DELETED
|
@@ -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
|
package/dist/parser.d.ts.map
DELETED
|
@@ -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"}
|