@atproto/oauth-scopes 0.5.2 → 0.5.3
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 +12 -0
- package/package.json +10 -6
- package/jest.config.cjs +0 -14
- package/src/atproto-oauth-scope.ts +0 -79
- package/src/index.ts +0 -13
- package/src/lib/lexicon.ts +0 -21
- package/src/lib/mime.test.ts +0 -98
- package/src/lib/mime.ts +0 -71
- package/src/lib/nsid.ts +0 -5
- package/src/lib/parser.ts +0 -176
- package/src/lib/resource-permission.ts +0 -10
- package/src/lib/syntax-lexicon.ts +0 -55
- package/src/lib/syntax-string.test.ts +0 -130
- package/src/lib/syntax-string.ts +0 -132
- package/src/lib/syntax.test.ts +0 -43
- package/src/lib/syntax.ts +0 -54
- package/src/lib/util.ts +0 -18
- package/src/scope-missing-error.ts +0 -15
- package/src/scope-permissions-transition.test.ts +0 -122
- package/src/scope-permissions-transition.ts +0 -71
- package/src/scope-permissions.test.ts +0 -303
- package/src/scope-permissions.ts +0 -91
- package/src/scopes/account-permission.test.ts +0 -187
- package/src/scopes/account-permission.ts +0 -78
- package/src/scopes/blob-permission.test.ts +0 -126
- package/src/scopes/blob-permission.ts +0 -105
- package/src/scopes/identity-permission.test.ts +0 -80
- package/src/scopes/identity-permission.ts +0 -54
- package/src/scopes/include-scope.test.ts +0 -637
- package/src/scopes/include-scope.ts +0 -208
- package/src/scopes/repo-permission.test.ts +0 -267
- package/src/scopes/repo-permission.ts +0 -111
- package/src/scopes/rpc-permission.test.ts +0 -323
- package/src/scopes/rpc-permission.ts +0 -90
- package/src/scopes-set.test.ts +0 -47
- package/src/scopes-set.ts +0 -134
- package/tsconfig.build.json +0 -9
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
|
@@ -1,130 +0,0 @@
|
|
|
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
|
-
})
|
package/src/lib/syntax-string.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
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
|
-
}
|
package/src/lib/syntax.test.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
})
|
package/src/lib/syntax.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
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): readonly ParamValue[] | null | undefined
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function isScopeSyntaxFor<P extends string>(
|
|
50
|
-
syntax: ScopeSyntax<string>,
|
|
51
|
-
prefix: P,
|
|
52
|
-
): syntax is ScopeSyntax<P> {
|
|
53
|
-
return syntax.prefix === prefix
|
|
54
|
-
}
|
package/src/lib/util.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface Matchable<T> {
|
|
2
|
-
matches(options: T): boolean
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function minIdx(a: number, b: number): number {
|
|
6
|
-
if (a === -1) return b
|
|
7
|
-
if (b === -1) return a
|
|
8
|
-
return Math.min(a, b)
|
|
9
|
-
}
|
|
10
|
-
|
|
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)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function isNonNullable<T>(value: T): value is NonNullable<T> {
|
|
17
|
-
return value != null
|
|
18
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export class ScopeMissingError extends Error {
|
|
2
|
-
name = 'ScopeMissingError'
|
|
3
|
-
|
|
4
|
-
// compatibility layer with http-errors package. The goal if to make
|
|
5
|
-
// isHttpError(new ScopeMissingError) return true.
|
|
6
|
-
status = 403
|
|
7
|
-
expose = true
|
|
8
|
-
get statusCode() {
|
|
9
|
-
return this.status
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
constructor(public readonly scope: string) {
|
|
13
|
-
super(`Missing required scope "${scope}"`)
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { ScopePermissionsTransition } from './scope-permissions-transition.js'
|
|
2
|
-
|
|
3
|
-
describe('ScopePermissionsTransition', () => {
|
|
4
|
-
describe('allowsAccount', () => {
|
|
5
|
-
it('should allow account:email with transition:email', () => {
|
|
6
|
-
const set = new ScopePermissionsTransition(
|
|
7
|
-
'transition:email account:repo',
|
|
8
|
-
)
|
|
9
|
-
expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(true)
|
|
10
|
-
expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
|
|
11
|
-
|
|
12
|
-
expect(set.allowsAccount({ attr: 'repo', action: 'read' })).toBe(true)
|
|
13
|
-
expect(set.allowsAccount({ attr: 'repo', action: 'manage' })).toBe(false)
|
|
14
|
-
|
|
15
|
-
expect(set.allowsAccount({ attr: 'status', action: 'read' })).toBe(false)
|
|
16
|
-
expect(set.allowsAccount({ attr: 'status', action: 'manage' })).toBe(
|
|
17
|
-
false,
|
|
18
|
-
)
|
|
19
|
-
})
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
describe('allowsBlob', () => {
|
|
23
|
-
it('should allow blob with transition:generic', () => {
|
|
24
|
-
const set = new ScopePermissionsTransition('transition:generic')
|
|
25
|
-
expect(set.allowsBlob({ mime: 'foo/bar' })).toBe(true)
|
|
26
|
-
})
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
describe('allowsRepo', () => {
|
|
30
|
-
it('should allow repo with transition:generic', () => {
|
|
31
|
-
const set = new ScopePermissionsTransition('transition:generic')
|
|
32
|
-
expect(
|
|
33
|
-
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
|
|
34
|
-
).toBe(true)
|
|
35
|
-
expect(
|
|
36
|
-
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
|
|
37
|
-
).toBe(true)
|
|
38
|
-
expect(
|
|
39
|
-
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
40
|
-
).toBe(true)
|
|
41
|
-
expect(
|
|
42
|
-
set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
|
|
43
|
-
).toBe(true)
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
describe('allowsRpc', () => {
|
|
48
|
-
it('should allow rpc with transition:generic', () => {
|
|
49
|
-
const set = new ScopePermissionsTransition('transition:generic')
|
|
50
|
-
expect(
|
|
51
|
-
set.allowsRpc({
|
|
52
|
-
aud: 'did:web:example.com',
|
|
53
|
-
lxm: 'app.bsky.feed.post',
|
|
54
|
-
}),
|
|
55
|
-
).toBe(true)
|
|
56
|
-
expect(
|
|
57
|
-
set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
|
|
58
|
-
).toBe(true)
|
|
59
|
-
expect(set.allowsRpc({ aud: 'did:web:example.com', lxm: '*' })).toBe(true)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('should allow chat.bsky.* methods with "transition:chat.bsky"', () => {
|
|
63
|
-
const set = new ScopePermissionsTransition('transition:chat.bsky')
|
|
64
|
-
expect(
|
|
65
|
-
set.allowsRpc({
|
|
66
|
-
aud: 'did:web:example.com',
|
|
67
|
-
lxm: 'chat.bsky.message.send',
|
|
68
|
-
}),
|
|
69
|
-
).toBe(true)
|
|
70
|
-
expect(
|
|
71
|
-
set.allowsRpc({
|
|
72
|
-
aud: 'did:web:example.com',
|
|
73
|
-
lxm: 'chat.bsky.conversation.get',
|
|
74
|
-
}),
|
|
75
|
-
).toBe(true)
|
|
76
|
-
|
|
77
|
-
// Control
|
|
78
|
-
|
|
79
|
-
expect(
|
|
80
|
-
set.allowsRpc({
|
|
81
|
-
aud: 'did:web:example.com',
|
|
82
|
-
lxm: 'app.bsky.feed.post',
|
|
83
|
-
}),
|
|
84
|
-
).toBe(false)
|
|
85
|
-
expect(
|
|
86
|
-
set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
|
|
87
|
-
).toBe(false)
|
|
88
|
-
expect(set.allowsRpc({ aud: 'did:web:example.com', lxm: '*' })).toBe(
|
|
89
|
-
false,
|
|
90
|
-
)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('should reject chat methods with "transition:generic"', () => {
|
|
94
|
-
const set = new ScopePermissionsTransition('transition:generic')
|
|
95
|
-
|
|
96
|
-
expect(
|
|
97
|
-
set.allowsRpc({
|
|
98
|
-
aud: 'did:web:example.com',
|
|
99
|
-
lxm: 'chat.bsky.message.send',
|
|
100
|
-
}),
|
|
101
|
-
).toBe(false)
|
|
102
|
-
expect(
|
|
103
|
-
set.allowsRpc({
|
|
104
|
-
aud: 'did:web:example.com',
|
|
105
|
-
lxm: 'chat.bsky.conversation.get',
|
|
106
|
-
}),
|
|
107
|
-
).toBe(false)
|
|
108
|
-
|
|
109
|
-
// Control
|
|
110
|
-
|
|
111
|
-
expect(
|
|
112
|
-
set.allowsRpc({
|
|
113
|
-
aud: 'did:web:example.com',
|
|
114
|
-
lxm: 'app.bsky.feed.post',
|
|
115
|
-
}),
|
|
116
|
-
).toBe(true)
|
|
117
|
-
expect(
|
|
118
|
-
set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
|
|
119
|
-
).toBe(true)
|
|
120
|
-
})
|
|
121
|
-
})
|
|
122
|
-
})
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AccountPermissionMatch,
|
|
3
|
-
BlobPermissionMatch,
|
|
4
|
-
RepoPermissionMatch,
|
|
5
|
-
RpcPermissionMatch,
|
|
6
|
-
ScopePermissions,
|
|
7
|
-
} from './scope-permissions.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Overrides the default permission set to allow transitional scopes to be used
|
|
11
|
-
* in place of the generic scopes.
|
|
12
|
-
*/
|
|
13
|
-
export class ScopePermissionsTransition extends ScopePermissions {
|
|
14
|
-
get hasTransitionGeneric(): boolean {
|
|
15
|
-
return this.scopes.has('transition:generic')
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
get hasTransitionEmail(): boolean {
|
|
19
|
-
return this.scopes.has('transition:email')
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
get hasTransitionChatBsky(): boolean {
|
|
23
|
-
return this.scopes.has('transition:chat.bsky')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
override allowsAccount(options: AccountPermissionMatch): boolean {
|
|
27
|
-
if (
|
|
28
|
-
options.attr === 'email' &&
|
|
29
|
-
options.action === 'read' &&
|
|
30
|
-
this.hasTransitionEmail
|
|
31
|
-
) {
|
|
32
|
-
return true
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return super.allowsAccount(options)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
override allowsBlob(options: BlobPermissionMatch): boolean {
|
|
39
|
-
if (this.hasTransitionGeneric) {
|
|
40
|
-
return true
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return super.allowsBlob(options)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
override allowsRepo(options: RepoPermissionMatch): boolean {
|
|
47
|
-
if (this.hasTransitionGeneric) {
|
|
48
|
-
return true
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return super.allowsRepo(options)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
override allowsRpc(options: RpcPermissionMatch) {
|
|
55
|
-
const { lxm } = options
|
|
56
|
-
|
|
57
|
-
if (this.hasTransitionGeneric && lxm === '*') {
|
|
58
|
-
return true
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (this.hasTransitionGeneric && !lxm.startsWith('chat.bsky.')) {
|
|
62
|
-
return true
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (this.hasTransitionChatBsky && lxm.startsWith('chat.bsky.')) {
|
|
66
|
-
return true
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return super.allowsRpc(options)
|
|
70
|
-
}
|
|
71
|
-
}
|