@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.
- package/CHANGELOG.md +31 -0
- package/dist/atproto-oauth-scope.d.ts +12 -0
- package/dist/atproto-oauth-scope.d.ts.map +1 -0
- package/dist/atproto-oauth-scope.js +32 -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 +4 -1
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +4 -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} +8 -6
- 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 +43 -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 +7 -12
- package/src/{permission-set-transition.test.ts → scope-permissions-transition.test.ts} +34 -21
- package/src/{permission-set-transition.ts → scope-permissions-transition.ts} +16 -12
- 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,130 @@
|
|
|
1
|
+
import { ScopeStringSyntax } from './syntax-string.js'
|
|
2
|
+
import { ScopeStringFor } from './syntax.js'
|
|
3
|
+
|
|
4
|
+
describe('ScopeStringSyntax', () => {
|
|
5
|
+
for (const { scope, content } of [
|
|
6
|
+
{
|
|
7
|
+
scope: 'my-res',
|
|
8
|
+
content: { prefix: 'my-res' },
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
scope: 'my-res:my-pos',
|
|
12
|
+
content: { prefix: 'my-res', positional: 'my-pos' },
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
scope: 'my-res:',
|
|
16
|
+
content: { prefix: 'my-res', positional: '' },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
scope: 'my-res:foo?x=value&y=value-y',
|
|
20
|
+
content: {
|
|
21
|
+
prefix: 'my-res',
|
|
22
|
+
positional: 'foo',
|
|
23
|
+
params: { x: ['value'], y: ['value-y'] },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
scope: 'my-res?x=value&y=value-y',
|
|
28
|
+
content: { prefix: 'my-res', params: { x: ['value'], y: ['value-y'] } },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
scope: 'my-res?x=foo&x=bar&x=baz',
|
|
32
|
+
content: { prefix: 'my-res', params: { x: ['foo', 'bar', 'baz'] } },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
scope: 'rpc:foo.bar?aud=did:foo:bar?lxm=bar.baz',
|
|
36
|
+
content: {
|
|
37
|
+
prefix: 'rpc',
|
|
38
|
+
positional: 'foo.bar',
|
|
39
|
+
params: { aud: ['did:foo:bar?lxm=bar.baz'] },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
] satisfies Array<{
|
|
43
|
+
scope: ScopeStringFor<'my-res' | 'rpc'>
|
|
44
|
+
content: {
|
|
45
|
+
prefix: string
|
|
46
|
+
positional?: string
|
|
47
|
+
params?: Record<string, string[]>
|
|
48
|
+
}
|
|
49
|
+
}>) {
|
|
50
|
+
const syntax = ScopeStringSyntax.fromString<'my-res' | 'rpc'>(scope)
|
|
51
|
+
|
|
52
|
+
describe(scope, () => {
|
|
53
|
+
it('should match the expected syntax', () => {
|
|
54
|
+
expect(syntax).toMatchObject({
|
|
55
|
+
prefix: content.prefix,
|
|
56
|
+
positional: content.positional,
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it(`should match ${scope} prefix`, () => {
|
|
61
|
+
expect(syntax.prefix).toBe(content.prefix)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it(`should return positional parameter`, () => {
|
|
65
|
+
expect(syntax.positional).toBe(content.positional)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it(`should return undefined for nonexistent single-value param`, () => {
|
|
69
|
+
expect(syntax.getSingle('nonexistent')).toBeUndefined()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it(`should return undefined for nonexistent multi-value param`, () => {
|
|
73
|
+
expect(syntax.getMulti('nonexistent')).toBeUndefined()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const { params } = content
|
|
77
|
+
if (params) {
|
|
78
|
+
it(`only contain allowed parameters`, () => {
|
|
79
|
+
const allowedParams = Object.keys(params) as [string, ...string[]]
|
|
80
|
+
expect(
|
|
81
|
+
Array.from(syntax.keys()).every((key) =>
|
|
82
|
+
allowedParams.includes(key),
|
|
83
|
+
),
|
|
84
|
+
).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
for (const [key, values] of Object.entries(params)) {
|
|
88
|
+
it(`should get an array when reading "${key}"`, () => {
|
|
89
|
+
expect(syntax.getMulti(key)).toEqual(values)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (values.length === 1) {
|
|
93
|
+
it(`should allow retrieving single-value params`, () => {
|
|
94
|
+
expect(syntax.getSingle(key)).toEqual(values[0])
|
|
95
|
+
})
|
|
96
|
+
} else {
|
|
97
|
+
it(`should return null for multi-value params`, () => {
|
|
98
|
+
expect(syntax.getSingle(key)).toBeNull()
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe('invalid positional parameters', () => {
|
|
107
|
+
it('should return null for positional parameters used together with named parameters', () => {
|
|
108
|
+
const syntax = ScopeStringSyntax.fromString('my-res:pos?x=value')
|
|
109
|
+
expect(syntax.getSingle('x')).toBe('value')
|
|
110
|
+
expect(syntax.getMulti('x')).toEqual(['value'])
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('url encoding', () => {
|
|
115
|
+
it('should handle URL encoding in positional parameters', () => {
|
|
116
|
+
const syntax = ScopeStringSyntax.fromString('my-res:my%20pos')
|
|
117
|
+
expect(syntax.positional).toBe('my pos')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should handle URL encoding in named parameters', () => {
|
|
121
|
+
const syntax = ScopeStringSyntax.fromString('my-res?x=my%20value')
|
|
122
|
+
expect(syntax.getSingle('x')).toBe('my value')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it(`should allow colon (:) in positional parameters`, () => {
|
|
126
|
+
const syntax = ScopeStringSyntax.fromString('my-res:my:pos')
|
|
127
|
+
expect(syntax.positional).toBe('my:pos')
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { ScopeStringFor, ScopeSyntax } from './syntax.js'
|
|
2
|
+
import { minIdx } from './util.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Translates a scope string into a {@link ScopeSyntax}.
|
|
6
|
+
*/
|
|
7
|
+
export class ScopeStringSyntax<P extends string> implements ScopeSyntax<P> {
|
|
8
|
+
constructor(
|
|
9
|
+
readonly prefix: P,
|
|
10
|
+
readonly positional?: string,
|
|
11
|
+
readonly params?: Readonly<URLSearchParams>,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
*keys() {
|
|
15
|
+
if (this.params) yield* this.params.keys()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getSingle(key: string) {
|
|
19
|
+
if (!this.params?.has(key)) return undefined
|
|
20
|
+
const value = this.params.getAll(key)
|
|
21
|
+
if (value.length > 1) return null
|
|
22
|
+
return value[0]!
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getMulti(key: string) {
|
|
26
|
+
if (!this.params?.has(key)) return undefined
|
|
27
|
+
return this.params.getAll(key)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
toString() {
|
|
31
|
+
let scope: string = this.prefix
|
|
32
|
+
|
|
33
|
+
if (this.positional !== undefined) {
|
|
34
|
+
scope += `:${normalizeURIComponent(encodeURIComponent(this.positional))}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (this.params?.size) {
|
|
38
|
+
scope += `?${normalizeURIComponent(this.params.toString())}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return scope as ScopeStringFor<P>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static fromString<P extends string>(
|
|
45
|
+
scopeValue: ScopeStringFor<P>,
|
|
46
|
+
): ScopeStringSyntax<P> {
|
|
47
|
+
const paramIdx = scopeValue.indexOf('?')
|
|
48
|
+
const colonIdx = scopeValue.indexOf(':')
|
|
49
|
+
const prefixEnd = minIdx(paramIdx, colonIdx)
|
|
50
|
+
|
|
51
|
+
// No param or positional
|
|
52
|
+
if (prefixEnd === -1) {
|
|
53
|
+
return new ScopeStringSyntax(scopeValue as P)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const prefix = scopeValue.slice(0, prefixEnd) as P
|
|
57
|
+
|
|
58
|
+
// Parse the positional parameter if present
|
|
59
|
+
const positional =
|
|
60
|
+
colonIdx !== -1
|
|
61
|
+
? paramIdx === -1
|
|
62
|
+
? decodeURIComponent(scopeValue.slice(colonIdx + 1))
|
|
63
|
+
: colonIdx < paramIdx
|
|
64
|
+
? decodeURIComponent(scopeValue.slice(colonIdx + 1, paramIdx))
|
|
65
|
+
: undefined
|
|
66
|
+
: undefined
|
|
67
|
+
|
|
68
|
+
// Parse the query string if present and non empty
|
|
69
|
+
const params =
|
|
70
|
+
paramIdx !== -1 && paramIdx < scopeValue.length - 1
|
|
71
|
+
? new URLSearchParams(scopeValue.slice(paramIdx + 1))
|
|
72
|
+
: undefined
|
|
73
|
+
|
|
74
|
+
return new ScopeStringSyntax(prefix, positional, params)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Set of characters that are allowed in scope components without encoding. This
|
|
80
|
+
* is used to normalize scope components.
|
|
81
|
+
*/
|
|
82
|
+
const ALLOWED_SCOPE_CHARS = new Set(
|
|
83
|
+
// @NOTE This list must not contain "?" or "&" as it would interfere with
|
|
84
|
+
// query string parsing.
|
|
85
|
+
[':', '/', '+', ',', '@', '%'],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const NORMALIZABLE_CHARS_MAP = new Map(
|
|
89
|
+
Array.from(
|
|
90
|
+
ALLOWED_SCOPE_CHARS,
|
|
91
|
+
(c) => [encodeURIComponent(c), c] as const,
|
|
92
|
+
).filter(
|
|
93
|
+
([encoded, c]) =>
|
|
94
|
+
// Make sure that any char added to ALLOWED_SCOPE_CHARS that is a char
|
|
95
|
+
// that indeed needs encoding. Also, the normalizeURIComponent only
|
|
96
|
+
// supports three-character percent-encoded sequences.
|
|
97
|
+
encoded !== c && encoded.length === 3 && encoded.startsWith('%'),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Assumes a properly url-encoded string.
|
|
103
|
+
*/
|
|
104
|
+
function normalizeURIComponent(value: string): string {
|
|
105
|
+
// No need to read the last two characters since percent encoded characters
|
|
106
|
+
// are always three characters long.
|
|
107
|
+
let end = value.length - 2
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < end; i++) {
|
|
110
|
+
// Check if the character is a percent-encoded character
|
|
111
|
+
if (value.charCodeAt(i) === 0x25 /* % */) {
|
|
112
|
+
// Read the next encoded char. Current version only supports
|
|
113
|
+
// three-character percent-encoded sequences.
|
|
114
|
+
const encodedChar = value.slice(i, i + 3)
|
|
115
|
+
|
|
116
|
+
// Check if the encoded character is in the normalization map
|
|
117
|
+
const normalizedChar = NORMALIZABLE_CHARS_MAP.get(encodedChar)
|
|
118
|
+
if (normalizedChar) {
|
|
119
|
+
// Replace the encoded character with its normalized version
|
|
120
|
+
value = `${value.slice(0, i)}${normalizedChar}${value.slice(i + encodedChar.length)}`
|
|
121
|
+
|
|
122
|
+
// Adjust index to account for the length change
|
|
123
|
+
i += normalizedChar.length - 1
|
|
124
|
+
|
|
125
|
+
// Adjust end index since we replaced encoded char with normalized char
|
|
126
|
+
end -= encodedChar.length - normalizedChar.length
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return value
|
|
132
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isScopeStringFor } from './syntax.js'
|
|
2
|
+
|
|
3
|
+
describe('isScopeStringFor', () => {
|
|
4
|
+
describe('exact match', () => {
|
|
5
|
+
it('should return true for exact match', () => {
|
|
6
|
+
expect(isScopeStringFor('prefix', 'prefix')).toBe(true)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('should return false for different prefix', () => {
|
|
10
|
+
expect(isScopeStringFor('prefix', 'differentResource')).toBe(false)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('with positional parameter', () => {
|
|
15
|
+
it('should return true for exact match with positional parameter', () => {
|
|
16
|
+
expect(isScopeStringFor('prefix:positional', 'prefix')).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should return false for different prefix with positional parameter', () => {
|
|
20
|
+
expect(isScopeStringFor('differentResource:positional', 'prefix')).toBe(
|
|
21
|
+
false,
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('with named parameters', () => {
|
|
27
|
+
it('should return true for exact match with named parameters', () => {
|
|
28
|
+
expect(isScopeStringFor('prefix?param=value', 'prefix')).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should return false for different prefix with named parameters', () => {
|
|
32
|
+
expect(isScopeStringFor('prefix', 'prefi')).toBe(false)
|
|
33
|
+
expect(isScopeStringFor('prefix:pos', 'prefi')).toBe(false)
|
|
34
|
+
expect(isScopeStringFor('prefix?param=value', 'prefi')).toBe(false)
|
|
35
|
+
expect(isScopeStringFor('prefix', 'fix')).toBe(false)
|
|
36
|
+
expect(isScopeStringFor('prefix:pos', 'fix')).toBe(false)
|
|
37
|
+
expect(isScopeStringFor('prefix?param=value', 'fix')).toBe(false)
|
|
38
|
+
expect(isScopeStringFor('differentResource?param=value', 'prefix')).toBe(
|
|
39
|
+
false,
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type ParamValue = string | number | boolean
|
|
2
|
+
|
|
3
|
+
export type NeArray<T> = [T, ...T[]]
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Non-empty readonly array
|
|
7
|
+
*/
|
|
8
|
+
export type NeRoArray<T> = readonly [T, ...T[]]
|
|
9
|
+
|
|
10
|
+
export type ScopeStringFor<P extends string> =
|
|
11
|
+
| P
|
|
12
|
+
| `${P}:${string}`
|
|
13
|
+
| `${P}?${string}`
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Allows to quickly check if a scope is for a specific resource.
|
|
17
|
+
*/
|
|
18
|
+
export function isScopeStringFor<P extends string>(
|
|
19
|
+
value: string,
|
|
20
|
+
prefix: P,
|
|
21
|
+
): value is ScopeStringFor<P> {
|
|
22
|
+
if (value.length > prefix.length) {
|
|
23
|
+
// First, check the next char is either : or ?
|
|
24
|
+
const nextChar = value.charCodeAt(prefix.length)
|
|
25
|
+
if (nextChar !== 0x3a /* : */ && nextChar !== 0x3f /* ? */) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Then check the full prefix
|
|
30
|
+
return value.startsWith(prefix)
|
|
31
|
+
} else {
|
|
32
|
+
// value and prefix must be equal
|
|
33
|
+
return value === prefix
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Abstract interface that allows parsing various syntaxes into permission
|
|
39
|
+
* representations.
|
|
40
|
+
*/
|
|
41
|
+
export interface ScopeSyntax<P extends string> {
|
|
42
|
+
readonly prefix: P
|
|
43
|
+
readonly positional?: ParamValue
|
|
44
|
+
keys(): Iterable<string, void, unknown>
|
|
45
|
+
getSingle(key: string): ParamValue | null | undefined
|
|
46
|
+
getMulti(key: string): ParamValue[] | null | undefined
|
|
47
|
+
}
|
package/src/lib/util.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
+
export interface Matchable<T> {
|
|
2
|
+
matches(options: T): boolean
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
export function minIdx(a: number, b: number): number {
|
|
2
6
|
if (a === -1) return b
|
|
3
7
|
if (b === -1) return a
|
|
4
8
|
return Math.min(a, b)
|
|
5
9
|
}
|
|
6
10
|
|
|
7
|
-
export function
|
|
8
|
-
|
|
9
|
-
):
|
|
10
|
-
const record: Record<string, [string, ...string[]]> = Object.create(null)
|
|
11
|
-
for (const [key, value] of iterable) {
|
|
12
|
-
if (Object.hasOwn(record, key)) {
|
|
13
|
-
record[key]!.push(value)
|
|
14
|
-
} else {
|
|
15
|
-
record[key] = [value]
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return record
|
|
11
|
+
export function knownValuesValidator<T>(values: Iterable<T>) {
|
|
12
|
+
const set = new Set<unknown>(values)
|
|
13
|
+
return (value: unknown): value is T => set.has(value)
|
|
19
14
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ScopePermissionsTransition } from './scope-permissions-transition.js'
|
|
2
2
|
|
|
3
|
-
describe('
|
|
3
|
+
describe('ScopePermissionsTransition', () => {
|
|
4
4
|
describe('allowsAccount', () => {
|
|
5
5
|
it('should allow account:email with transition:email', () => {
|
|
6
|
-
const set = new
|
|
6
|
+
const set = new ScopePermissionsTransition(
|
|
7
|
+
'transition:email account:repo',
|
|
8
|
+
)
|
|
7
9
|
expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(true)
|
|
8
|
-
expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(
|
|
10
|
+
expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
|
|
9
11
|
|
|
10
12
|
expect(set.allowsAccount({ attr: 'repo', action: 'read' })).toBe(true)
|
|
11
13
|
expect(set.allowsAccount({ attr: 'repo', action: 'manage' })).toBe(false)
|
|
@@ -19,14 +21,14 @@ describe('PermissionSetTransition', () => {
|
|
|
19
21
|
|
|
20
22
|
describe('allowsBlob', () => {
|
|
21
23
|
it('should allow blob with transition:generic', () => {
|
|
22
|
-
const set = new
|
|
24
|
+
const set = new ScopePermissionsTransition('transition:generic')
|
|
23
25
|
expect(set.allowsBlob({ mime: 'foo/bar' })).toBe(true)
|
|
24
26
|
})
|
|
25
27
|
})
|
|
26
28
|
|
|
27
29
|
describe('allowsRepo', () => {
|
|
28
30
|
it('should allow repo with transition:generic', () => {
|
|
29
|
-
const set = new
|
|
31
|
+
const set = new ScopePermissionsTransition('transition:generic')
|
|
30
32
|
expect(
|
|
31
33
|
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
|
|
32
34
|
).toBe(true)
|
|
@@ -44,27 +46,30 @@ describe('PermissionSetTransition', () => {
|
|
|
44
46
|
|
|
45
47
|
describe('allowsRpc', () => {
|
|
46
48
|
it('should allow rpc with transition:generic', () => {
|
|
47
|
-
const set = new
|
|
49
|
+
const set = new ScopePermissionsTransition('transition:generic')
|
|
48
50
|
expect(
|
|
49
|
-
set.allowsRpc({
|
|
51
|
+
set.allowsRpc({
|
|
52
|
+
aud: 'did:web:example.com',
|
|
53
|
+
lxm: 'app.bsky.feed.post',
|
|
54
|
+
}),
|
|
50
55
|
).toBe(true)
|
|
51
56
|
expect(
|
|
52
|
-
set.allowsRpc({ aud: 'did:example
|
|
57
|
+
set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
|
|
53
58
|
).toBe(true)
|
|
54
|
-
expect(set.allowsRpc({ aud: 'did:example
|
|
59
|
+
expect(set.allowsRpc({ aud: 'did:web:example.com', lxm: '*' })).toBe(true)
|
|
55
60
|
})
|
|
56
61
|
|
|
57
62
|
it('should allow chat.bsky.* methods with "transition:chat.bsky"', () => {
|
|
58
|
-
const set = new
|
|
63
|
+
const set = new ScopePermissionsTransition('transition:chat.bsky')
|
|
59
64
|
expect(
|
|
60
65
|
set.allowsRpc({
|
|
61
|
-
aud: 'did:example
|
|
66
|
+
aud: 'did:web:example.com',
|
|
62
67
|
lxm: 'chat.bsky.message.send',
|
|
63
68
|
}),
|
|
64
69
|
).toBe(true)
|
|
65
70
|
expect(
|
|
66
71
|
set.allowsRpc({
|
|
67
|
-
aud: 'did:example
|
|
72
|
+
aud: 'did:web:example.com',
|
|
68
73
|
lxm: 'chat.bsky.conversation.get',
|
|
69
74
|
}),
|
|
70
75
|
).toBe(true)
|
|
@@ -72,26 +77,31 @@ describe('PermissionSetTransition', () => {
|
|
|
72
77
|
// Control
|
|
73
78
|
|
|
74
79
|
expect(
|
|
75
|
-
set.allowsRpc({
|
|
80
|
+
set.allowsRpc({
|
|
81
|
+
aud: 'did:web:example.com',
|
|
82
|
+
lxm: 'app.bsky.feed.post',
|
|
83
|
+
}),
|
|
76
84
|
).toBe(false)
|
|
77
85
|
expect(
|
|
78
|
-
set.allowsRpc({ aud: 'did:example
|
|
86
|
+
set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
|
|
79
87
|
).toBe(false)
|
|
80
|
-
expect(set.allowsRpc({ aud: 'did:example
|
|
88
|
+
expect(set.allowsRpc({ aud: 'did:web:example.com', lxm: '*' })).toBe(
|
|
89
|
+
false,
|
|
90
|
+
)
|
|
81
91
|
})
|
|
82
92
|
|
|
83
93
|
it('should reject chat methods with "transition:generic"', () => {
|
|
84
|
-
const set = new
|
|
94
|
+
const set = new ScopePermissionsTransition('transition:generic')
|
|
85
95
|
|
|
86
96
|
expect(
|
|
87
97
|
set.allowsRpc({
|
|
88
|
-
aud: 'did:example
|
|
98
|
+
aud: 'did:web:example.com',
|
|
89
99
|
lxm: 'chat.bsky.message.send',
|
|
90
100
|
}),
|
|
91
101
|
).toBe(false)
|
|
92
102
|
expect(
|
|
93
103
|
set.allowsRpc({
|
|
94
|
-
aud: 'did:example
|
|
104
|
+
aud: 'did:web:example.com',
|
|
95
105
|
lxm: 'chat.bsky.conversation.get',
|
|
96
106
|
}),
|
|
97
107
|
).toBe(false)
|
|
@@ -99,10 +109,13 @@ describe('PermissionSetTransition', () => {
|
|
|
99
109
|
// Control
|
|
100
110
|
|
|
101
111
|
expect(
|
|
102
|
-
set.allowsRpc({
|
|
112
|
+
set.allowsRpc({
|
|
113
|
+
aud: 'did:web:example.com',
|
|
114
|
+
lxm: 'app.bsky.feed.post',
|
|
115
|
+
}),
|
|
103
116
|
).toBe(true)
|
|
104
117
|
expect(
|
|
105
|
-
set.allowsRpc({ aud: 'did:example
|
|
118
|
+
set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
|
|
106
119
|
).toBe(true)
|
|
107
120
|
})
|
|
108
121
|
})
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from './
|
|
2
|
+
AccountPermissionMatch,
|
|
3
|
+
BlobPermissionMatch,
|
|
4
|
+
RepoPermissionMatch,
|
|
5
|
+
RpcPermissionMatch,
|
|
6
|
+
ScopePermissions,
|
|
7
|
+
} from './scope-permissions.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Overrides the default permission set to allow transitional scopes to be used
|
|
11
11
|
* in place of the generic scopes.
|
|
12
12
|
*/
|
|
13
|
-
export class
|
|
13
|
+
export class ScopePermissionsTransition extends ScopePermissions {
|
|
14
14
|
get hasTransitionGeneric(): boolean {
|
|
15
15
|
return this.scopes.has('transition:generic')
|
|
16
16
|
}
|
|
@@ -23,15 +23,19 @@ export class PermissionSetTransition extends PermissionSet {
|
|
|
23
23
|
return this.scopes.has('transition:chat.bsky')
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
override allowsAccount(options:
|
|
27
|
-
if (
|
|
26
|
+
override allowsAccount(options: AccountPermissionMatch): boolean {
|
|
27
|
+
if (
|
|
28
|
+
options.attr === 'email' &&
|
|
29
|
+
options.action === 'read' &&
|
|
30
|
+
this.hasTransitionEmail
|
|
31
|
+
) {
|
|
28
32
|
return true
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
return super.allowsAccount(options)
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
override allowsBlob(options:
|
|
38
|
+
override allowsBlob(options: BlobPermissionMatch): boolean {
|
|
35
39
|
if (this.hasTransitionGeneric) {
|
|
36
40
|
return true
|
|
37
41
|
}
|
|
@@ -39,7 +43,7 @@ export class PermissionSetTransition extends PermissionSet {
|
|
|
39
43
|
return super.allowsBlob(options)
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
override allowsRepo(options:
|
|
46
|
+
override allowsRepo(options: RepoPermissionMatch): boolean {
|
|
43
47
|
if (this.hasTransitionGeneric) {
|
|
44
48
|
return true
|
|
45
49
|
}
|
|
@@ -47,7 +51,7 @@ export class PermissionSetTransition extends PermissionSet {
|
|
|
47
51
|
return super.allowsRepo(options)
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
override allowsRpc(options:
|
|
54
|
+
override allowsRpc(options: RpcPermissionMatch) {
|
|
51
55
|
const { lxm } = options
|
|
52
56
|
|
|
53
57
|
if (this.hasTransitionGeneric && lxm === '*') {
|