@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
package/src/lib/mime.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// @TODO Refactor in shared location for use with other @atproto packages
|
|
2
|
+
|
|
3
|
+
function isStringSlashString(value: string): value is `${string}/${string}` {
|
|
4
|
+
const slashIndex = value.indexOf('/')
|
|
5
|
+
|
|
6
|
+
if (slashIndex === -1) return false // Missing slash
|
|
7
|
+
if (slashIndex === 0) return false // No leading part before the slash
|
|
8
|
+
if (slashIndex === value.length - 1) return false // No trailing part after the slash
|
|
9
|
+
if (value.includes('/', slashIndex + 1)) return false // More than one slash
|
|
10
|
+
if (value.includes(' ')) return false // Spaces are not allowed
|
|
11
|
+
|
|
12
|
+
return true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Mime = `${string}/${string}`
|
|
16
|
+
|
|
17
|
+
export function isMime(value: string): value is Mime {
|
|
18
|
+
return isStringSlashString(value) && !value.includes('*')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type Accept = '*/*' | `${string}/*` | Mime
|
|
22
|
+
|
|
23
|
+
export function isAccept(value: string): value is Accept {
|
|
24
|
+
if (value === '*/*') return true // Fast path for the most common case
|
|
25
|
+
if (!isStringSlashString(value)) return false
|
|
26
|
+
return !value.includes('*') || value.endsWith('/*')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @note "unsafe" in that it does not check if either {@link accept} or
|
|
31
|
+
* {@link mime} are actually valid values (and could, therefore, lead to false
|
|
32
|
+
* positives if forged values are used).
|
|
33
|
+
*/
|
|
34
|
+
function matchesAcceptUnsafe(accept: Accept, mime: Mime): boolean {
|
|
35
|
+
if (accept === '*/*') {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
if (accept.endsWith('/*')) {
|
|
39
|
+
return mime.startsWith(accept.slice(0, -1))
|
|
40
|
+
}
|
|
41
|
+
return accept === mime
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function matchesAccept(accept: Accept, mime: string): boolean {
|
|
45
|
+
return isMime(mime) && matchesAcceptUnsafe(accept, mime)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @note "unsafe" in that it does not check if either {@link accept} or
|
|
50
|
+
* {@link mime} are actually valid values (and could, therefore, lead to false
|
|
51
|
+
* positives if forged values are used).
|
|
52
|
+
*/
|
|
53
|
+
function matchesAnyAcceptUnsafe(
|
|
54
|
+
acceptable: Iterable<Accept>,
|
|
55
|
+
mime: Mime,
|
|
56
|
+
): boolean {
|
|
57
|
+
for (const accept of acceptable) {
|
|
58
|
+
if (matchesAcceptUnsafe(accept, mime)) {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function matchesAnyAccept(
|
|
66
|
+
acceptable: Iterable<Accept>,
|
|
67
|
+
mime: string,
|
|
68
|
+
): boolean {
|
|
69
|
+
return isMime(mime) && matchesAnyAcceptUnsafe(acceptable, mime)
|
|
70
|
+
}
|
package/src/lib/nsid.ts
ADDED
package/src/lib/util.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function minIdx(a: number, b: number): number {
|
|
2
|
+
if (a === -1) return b
|
|
3
|
+
if (b === -1) return a
|
|
4
|
+
return Math.min(a, b)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function toRecord(
|
|
8
|
+
iterable: Iterable<[key: string, value: string]>,
|
|
9
|
+
): Record<string, [string, ...string[]]> {
|
|
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
|
|
19
|
+
}
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NeRoArray,
|
|
3
|
+
ResourceSyntax,
|
|
4
|
+
ScopeForResource,
|
|
5
|
+
formatScope,
|
|
6
|
+
} from './syntax.js'
|
|
7
|
+
|
|
8
|
+
type InferStringPredicate<T extends undefined | ((value: string) => boolean)> =
|
|
9
|
+
T extends ((value: string) => value is infer U extends string) ? U : string
|
|
10
|
+
|
|
11
|
+
type ParamsSchema = Record<
|
|
12
|
+
string,
|
|
13
|
+
| {
|
|
14
|
+
multiple: false
|
|
15
|
+
required: boolean
|
|
16
|
+
default?: string
|
|
17
|
+
normalize?: (value: string) => string
|
|
18
|
+
validate?: (value: string) => boolean
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
multiple: true
|
|
22
|
+
required: boolean
|
|
23
|
+
default?: NeRoArray<string>
|
|
24
|
+
normalize?: (value: NeRoArray<string>) => NeRoArray<string>
|
|
25
|
+
validate?: (value: string) => boolean
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
type ParsedParams<S extends ParamsSchema> = {
|
|
30
|
+
[K in keyof S]:
|
|
31
|
+
| (S[K]['required'] extends true
|
|
32
|
+
? never
|
|
33
|
+
: 'default' extends keyof S[K]
|
|
34
|
+
? S[K]['default']
|
|
35
|
+
: undefined)
|
|
36
|
+
| (S[K]['multiple'] extends true
|
|
37
|
+
? NeRoArray<InferStringPredicate<S[K]['validate']>>
|
|
38
|
+
: InferStringPredicate<S[K]['validate']>)
|
|
39
|
+
} & NonNullable<unknown>
|
|
40
|
+
|
|
41
|
+
export class Parser<R extends string, S extends ParamsSchema> {
|
|
42
|
+
readonly schemaKeys: ReadonlyArray<keyof S & string>
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
readonly resource: R,
|
|
46
|
+
readonly schema: S,
|
|
47
|
+
readonly positionalName?: keyof S & string,
|
|
48
|
+
) {
|
|
49
|
+
this.schemaKeys = Object.keys(schema)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
format(values: ParsedParams<S>): ScopeForResource<R> {
|
|
53
|
+
// Build params
|
|
54
|
+
const params: [
|
|
55
|
+
name: string,
|
|
56
|
+
value: undefined | string | NeRoArray<string>,
|
|
57
|
+
][] = []
|
|
58
|
+
|
|
59
|
+
for (const key of this.schemaKeys) {
|
|
60
|
+
const value = values[key]
|
|
61
|
+
// Ignore undefined values
|
|
62
|
+
if (value === undefined) continue
|
|
63
|
+
|
|
64
|
+
const schema = this.schema[key]
|
|
65
|
+
|
|
66
|
+
// @TODO: when the value is an array, we could remove duplicates
|
|
67
|
+
|
|
68
|
+
// Normalize the value if a normalization function is provided
|
|
69
|
+
const normalized = schema.normalize
|
|
70
|
+
? schema.normalize(value as any)
|
|
71
|
+
: value
|
|
72
|
+
|
|
73
|
+
// Ignore values that are equal to the default value
|
|
74
|
+
if (!schema.required) {
|
|
75
|
+
if (schema.default === normalized) continue
|
|
76
|
+
if (
|
|
77
|
+
schema.multiple &&
|
|
78
|
+
schema.default &&
|
|
79
|
+
arrayParamEquals(schema.default, normalized as NeRoArray<string>)
|
|
80
|
+
) {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
params.push([key, normalized])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return formatScope<R>(this.resource, params, this.positionalName)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
parse(syntax: ResourceSyntax) {
|
|
92
|
+
if (!syntax.is(this.resource)) return null
|
|
93
|
+
if (syntax.containsParamsOtherThan(this.schemaKeys)) return null
|
|
94
|
+
|
|
95
|
+
const result: Record<string, undefined | string | NeRoArray<string>> =
|
|
96
|
+
Object.create(null)
|
|
97
|
+
|
|
98
|
+
for (const key of this.schemaKeys) {
|
|
99
|
+
const definition = this.schema[key]
|
|
100
|
+
|
|
101
|
+
const value = definition.multiple
|
|
102
|
+
? syntax.getMulti(key, key === this.positionalName)
|
|
103
|
+
: syntax.getSingle(key, key === this.positionalName)
|
|
104
|
+
|
|
105
|
+
if (value === null) return null // Value is not valid
|
|
106
|
+
if (value === undefined && definition.required) return null
|
|
107
|
+
|
|
108
|
+
if (value !== undefined && definition.validate) {
|
|
109
|
+
if (definition.multiple) {
|
|
110
|
+
if (!(value as NeRoArray<string>).every(definition.validate)) {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
if (!definition.validate(value as string)) {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
result[key] = value ?? definition.default
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result as ParsedParams<S>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
parseString(scope: string): ParsedParams<S> | null {
|
|
127
|
+
const syntax = ResourceSyntax.fromString(scope)
|
|
128
|
+
return this.parse(syntax)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Two param arrays are considered equal if they contain the same values,
|
|
134
|
+
* regardless of the order and duplicates.
|
|
135
|
+
* @param a - The first array to compare.
|
|
136
|
+
* @param b - The second array to compare.
|
|
137
|
+
*/
|
|
138
|
+
function arrayParamEquals(
|
|
139
|
+
a: readonly unknown[],
|
|
140
|
+
b: readonly unknown[],
|
|
141
|
+
): boolean {
|
|
142
|
+
for (const item of a) if (!b.includes(item)) return false
|
|
143
|
+
for (const item of b) if (!a.includes(item)) return false
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function knownValuesValidator<T extends string>(values: Iterable<T>) {
|
|
148
|
+
const set = new Set<string>(values)
|
|
149
|
+
return (value: string): value is T => set.has(value)
|
|
150
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { PermissionSetTransition } from './permission-set-transition.js'
|
|
2
|
+
|
|
3
|
+
describe('PermissionSetTransition', () => {
|
|
4
|
+
describe('allowsAccount', () => {
|
|
5
|
+
it('should allow account:email with transition:email', () => {
|
|
6
|
+
const set = new PermissionSetTransition('transition:email account:repo')
|
|
7
|
+
expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(true)
|
|
8
|
+
expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(true)
|
|
9
|
+
|
|
10
|
+
expect(set.allowsAccount({ attr: 'repo', action: 'read' })).toBe(true)
|
|
11
|
+
expect(set.allowsAccount({ attr: 'repo', action: 'manage' })).toBe(false)
|
|
12
|
+
|
|
13
|
+
expect(set.allowsAccount({ attr: 'status', action: 'read' })).toBe(false)
|
|
14
|
+
expect(set.allowsAccount({ attr: 'status', action: 'manage' })).toBe(
|
|
15
|
+
false,
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('allowsBlob', () => {
|
|
21
|
+
it('should allow blob with transition:generic', () => {
|
|
22
|
+
const set = new PermissionSetTransition('transition:generic')
|
|
23
|
+
expect(set.allowsBlob({ mime: 'foo/bar' })).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('allowsRepo', () => {
|
|
28
|
+
it('should allow repo with transition:generic', () => {
|
|
29
|
+
const set = new PermissionSetTransition('transition:generic')
|
|
30
|
+
expect(
|
|
31
|
+
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
|
|
32
|
+
).toBe(true)
|
|
33
|
+
expect(
|
|
34
|
+
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
|
|
35
|
+
).toBe(true)
|
|
36
|
+
expect(
|
|
37
|
+
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
38
|
+
).toBe(true)
|
|
39
|
+
expect(
|
|
40
|
+
set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
|
|
41
|
+
).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('allowsRpc', () => {
|
|
46
|
+
it('should allow rpc with transition:generic', () => {
|
|
47
|
+
const set = new PermissionSetTransition('transition:generic')
|
|
48
|
+
expect(
|
|
49
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.post' }),
|
|
50
|
+
).toBe(true)
|
|
51
|
+
expect(
|
|
52
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.foo' }),
|
|
53
|
+
).toBe(true)
|
|
54
|
+
expect(set.allowsRpc({ aud: 'did:example:123', lxm: '*' })).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should allow chat.bsky.* methods with "transition:chat.bsky"', () => {
|
|
58
|
+
const set = new PermissionSetTransition('transition:chat.bsky')
|
|
59
|
+
expect(
|
|
60
|
+
set.allowsRpc({
|
|
61
|
+
aud: 'did:example:123',
|
|
62
|
+
lxm: 'chat.bsky.message.send',
|
|
63
|
+
}),
|
|
64
|
+
).toBe(true)
|
|
65
|
+
expect(
|
|
66
|
+
set.allowsRpc({
|
|
67
|
+
aud: 'did:example:123',
|
|
68
|
+
lxm: 'chat.bsky.conversation.get',
|
|
69
|
+
}),
|
|
70
|
+
).toBe(true)
|
|
71
|
+
|
|
72
|
+
// Control
|
|
73
|
+
|
|
74
|
+
expect(
|
|
75
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.post' }),
|
|
76
|
+
).toBe(false)
|
|
77
|
+
expect(
|
|
78
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.foo' }),
|
|
79
|
+
).toBe(false)
|
|
80
|
+
expect(set.allowsRpc({ aud: 'did:example:123', lxm: '*' })).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should reject chat methods with "transition:generic"', () => {
|
|
84
|
+
const set = new PermissionSetTransition('transition:generic')
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
set.allowsRpc({
|
|
88
|
+
aud: 'did:example:123',
|
|
89
|
+
lxm: 'chat.bsky.message.send',
|
|
90
|
+
}),
|
|
91
|
+
).toBe(false)
|
|
92
|
+
expect(
|
|
93
|
+
set.allowsRpc({
|
|
94
|
+
aud: 'did:example:123',
|
|
95
|
+
lxm: 'chat.bsky.conversation.get',
|
|
96
|
+
}),
|
|
97
|
+
).toBe(false)
|
|
98
|
+
|
|
99
|
+
// Control
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.post' }),
|
|
103
|
+
).toBe(true)
|
|
104
|
+
expect(
|
|
105
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.foo' }),
|
|
106
|
+
).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccountScopeMatch,
|
|
3
|
+
BlobScopeMatch,
|
|
4
|
+
PermissionSet,
|
|
5
|
+
RepoScopeMatch,
|
|
6
|
+
RpcScopeMatch,
|
|
7
|
+
} from './permission-set.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 PermissionSetTransition extends PermissionSet {
|
|
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: AccountScopeMatch): boolean {
|
|
27
|
+
if (options.attr === 'email' && this.hasTransitionEmail) {
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return super.allowsAccount(options)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override allowsBlob(options: BlobScopeMatch): boolean {
|
|
35
|
+
if (this.hasTransitionGeneric) {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return super.allowsBlob(options)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override allowsRepo(options: RepoScopeMatch): boolean {
|
|
43
|
+
if (this.hasTransitionGeneric) {
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return super.allowsRepo(options)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override allowsRpc(options: RpcScopeMatch) {
|
|
51
|
+
const { lxm } = options
|
|
52
|
+
|
|
53
|
+
if (this.hasTransitionGeneric && lxm === '*') {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.hasTransitionGeneric && !lxm.startsWith('chat.bsky.')) {
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.hasTransitionChatBsky && lxm.startsWith('chat.bsky.')) {
|
|
62
|
+
return true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return super.allowsRpc(options)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { PermissionSet } from './permission-set.js'
|
|
2
|
+
|
|
3
|
+
describe('PermissionSet', () => {
|
|
4
|
+
describe('allowsAccount', () => {
|
|
5
|
+
it('should properly allow "account:email"', () => {
|
|
6
|
+
const set = new PermissionSet('account:email')
|
|
7
|
+
|
|
8
|
+
expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(true)
|
|
9
|
+
expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
|
|
10
|
+
|
|
11
|
+
expect(set.allowsAccount({ attr: 'repo', action: 'read' })).toBe(false)
|
|
12
|
+
expect(set.allowsAccount({ attr: 'repo', action: 'manage' })).toBe(false)
|
|
13
|
+
|
|
14
|
+
expect(set.allowsAccount({ attr: 'status', action: 'read' })).toBe(false)
|
|
15
|
+
expect(set.allowsAccount({ attr: 'status', action: 'manage' })).toBe(
|
|
16
|
+
false,
|
|
17
|
+
)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should ignore "transition:email"', () => {
|
|
21
|
+
const set = new PermissionSet('transition:email')
|
|
22
|
+
|
|
23
|
+
expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(false)
|
|
24
|
+
expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('allowsBlob', () => {
|
|
29
|
+
it('should allow any mime with "blob:*/*"', () => {
|
|
30
|
+
const set = new PermissionSet('blob:*/*')
|
|
31
|
+
expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)
|
|
32
|
+
expect(set.allowsBlob({ mime: 'application/json' })).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should only allow images with "blob:image/*"', () => {
|
|
36
|
+
const set = new PermissionSet('blob:image/*')
|
|
37
|
+
expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)
|
|
38
|
+
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should ignore invalid scope "blob:*"', () => {
|
|
42
|
+
const set = new PermissionSet('blob:*')
|
|
43
|
+
expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
|
|
44
|
+
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should ignore invalid scope "blob:/image"', () => {
|
|
48
|
+
const set = new PermissionSet('blob:/image')
|
|
49
|
+
expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
|
|
50
|
+
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should ignore "transition:generic"', () => {
|
|
54
|
+
const set = new PermissionSet('transition:generic')
|
|
55
|
+
expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
|
|
56
|
+
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('allowsRepo', () => {
|
|
61
|
+
it('should allow any repo action with "repo:*"', () => {
|
|
62
|
+
const set = new PermissionSet('repo:*')
|
|
63
|
+
expect(
|
|
64
|
+
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
65
|
+
).toBe(true)
|
|
66
|
+
expect(
|
|
67
|
+
set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
|
|
68
|
+
).toBe(true)
|
|
69
|
+
expect(
|
|
70
|
+
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
|
|
71
|
+
).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should allow specific repo actions', () => {
|
|
75
|
+
const set = new PermissionSet('repo:*?action=create')
|
|
76
|
+
expect(
|
|
77
|
+
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
78
|
+
).toBe(true)
|
|
79
|
+
expect(
|
|
80
|
+
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
|
|
81
|
+
).toBe(true)
|
|
82
|
+
|
|
83
|
+
// Control
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
|
|
87
|
+
).toBe(false)
|
|
88
|
+
expect(
|
|
89
|
+
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
|
|
90
|
+
).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should allow specific repo collection & actions', () => {
|
|
94
|
+
const set = new PermissionSet('repo:com.example.foo?action=create')
|
|
95
|
+
expect(
|
|
96
|
+
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
97
|
+
).toBe(true)
|
|
98
|
+
|
|
99
|
+
// Control
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
|
|
103
|
+
).toBe(false)
|
|
104
|
+
expect(
|
|
105
|
+
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
|
|
106
|
+
).toBe(false)
|
|
107
|
+
expect(
|
|
108
|
+
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
|
|
109
|
+
).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should ignore transition:generic', () => {
|
|
113
|
+
const set = new PermissionSet('transition:generic')
|
|
114
|
+
expect(
|
|
115
|
+
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
|
|
116
|
+
).toBe(false)
|
|
117
|
+
expect(
|
|
118
|
+
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
|
|
119
|
+
).toBe(false)
|
|
120
|
+
expect(
|
|
121
|
+
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
122
|
+
).toBe(false)
|
|
123
|
+
expect(
|
|
124
|
+
set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
|
|
125
|
+
).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('allowsRpc', () => {
|
|
130
|
+
it('should ignore "rpc:*?lxm=*"', () => {
|
|
131
|
+
const set = new PermissionSet('rpc:*?lxm=*')
|
|
132
|
+
expect(
|
|
133
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.method' }),
|
|
134
|
+
).toBe(false)
|
|
135
|
+
expect(
|
|
136
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.getFeed' }),
|
|
137
|
+
).toBe(false)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should allow constraining "lxm"', () => {
|
|
141
|
+
const set = new PermissionSet('rpc:app.bsky.feed.getFeed?aud=*')
|
|
142
|
+
expect(
|
|
143
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.getFeed' }),
|
|
144
|
+
).toBe(true)
|
|
145
|
+
expect(
|
|
146
|
+
set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),
|
|
147
|
+
).toBe(true)
|
|
148
|
+
|
|
149
|
+
// Control
|
|
150
|
+
|
|
151
|
+
expect(
|
|
152
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.method' }),
|
|
153
|
+
).toBe(false)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should allow constraining "aud"', () => {
|
|
157
|
+
const set = new PermissionSet('rpc:*?aud=did:example:123')
|
|
158
|
+
expect(
|
|
159
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.method' }),
|
|
160
|
+
).toBe(true)
|
|
161
|
+
expect(
|
|
162
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.getFeed' }),
|
|
163
|
+
).toBe(true)
|
|
164
|
+
|
|
165
|
+
// Control
|
|
166
|
+
|
|
167
|
+
expect(
|
|
168
|
+
set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'com.example.method' }),
|
|
169
|
+
).toBe(false)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should allow constraining "lxm" and "aud"', () => {
|
|
173
|
+
const set = new PermissionSet(
|
|
174
|
+
'rpc:app.bsky.feed.getFeed?aud=did:example:123',
|
|
175
|
+
)
|
|
176
|
+
expect(
|
|
177
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.getFeed' }),
|
|
178
|
+
).toBe(true)
|
|
179
|
+
|
|
180
|
+
// Control
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.method' }),
|
|
184
|
+
).toBe(false)
|
|
185
|
+
expect(
|
|
186
|
+
set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),
|
|
187
|
+
).toBe(false)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should ignore "transition:generic"', () => {
|
|
191
|
+
const set = new PermissionSet('transition:generic')
|
|
192
|
+
expect(
|
|
193
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.getFeed' }),
|
|
194
|
+
).toBe(false)
|
|
195
|
+
expect(
|
|
196
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.method' }),
|
|
197
|
+
).toBe(false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should ignore "transition:chat.bsky"', () => {
|
|
201
|
+
const set = new PermissionSet('transition:chat.bsky')
|
|
202
|
+
expect(
|
|
203
|
+
set.allowsRpc({
|
|
204
|
+
aud: 'did:example:123',
|
|
205
|
+
lxm: 'chat.bsky.message.send',
|
|
206
|
+
}),
|
|
207
|
+
).toBe(false)
|
|
208
|
+
expect(
|
|
209
|
+
set.allowsRpc({
|
|
210
|
+
aud: 'did:example:123',
|
|
211
|
+
lxm: 'chat.bsky.conversation.get',
|
|
212
|
+
}),
|
|
213
|
+
).toBe(false)
|
|
214
|
+
|
|
215
|
+
// Control
|
|
216
|
+
|
|
217
|
+
expect(
|
|
218
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.post' }),
|
|
219
|
+
).toBe(false)
|
|
220
|
+
expect(
|
|
221
|
+
set.allowsRpc({ aud: 'did:example:123', lxm: 'com.example.foo' }),
|
|
222
|
+
).toBe(false)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|