@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +10 -6
  3. package/jest.config.cjs +0 -14
  4. package/src/atproto-oauth-scope.ts +0 -79
  5. package/src/index.ts +0 -13
  6. package/src/lib/lexicon.ts +0 -21
  7. package/src/lib/mime.test.ts +0 -98
  8. package/src/lib/mime.ts +0 -71
  9. package/src/lib/nsid.ts +0 -5
  10. package/src/lib/parser.ts +0 -176
  11. package/src/lib/resource-permission.ts +0 -10
  12. package/src/lib/syntax-lexicon.ts +0 -55
  13. package/src/lib/syntax-string.test.ts +0 -130
  14. package/src/lib/syntax-string.ts +0 -132
  15. package/src/lib/syntax.test.ts +0 -43
  16. package/src/lib/syntax.ts +0 -54
  17. package/src/lib/util.ts +0 -18
  18. package/src/scope-missing-error.ts +0 -15
  19. package/src/scope-permissions-transition.test.ts +0 -122
  20. package/src/scope-permissions-transition.ts +0 -71
  21. package/src/scope-permissions.test.ts +0 -303
  22. package/src/scope-permissions.ts +0 -91
  23. package/src/scopes/account-permission.test.ts +0 -187
  24. package/src/scopes/account-permission.ts +0 -78
  25. package/src/scopes/blob-permission.test.ts +0 -126
  26. package/src/scopes/blob-permission.ts +0 -105
  27. package/src/scopes/identity-permission.test.ts +0 -80
  28. package/src/scopes/identity-permission.ts +0 -54
  29. package/src/scopes/include-scope.test.ts +0 -637
  30. package/src/scopes/include-scope.ts +0 -208
  31. package/src/scopes/repo-permission.test.ts +0 -267
  32. package/src/scopes/repo-permission.ts +0 -111
  33. package/src/scopes/rpc-permission.test.ts +0 -323
  34. package/src/scopes/rpc-permission.ts +0 -90
  35. package/src/scopes-set.test.ts +0 -47
  36. package/src/scopes-set.ts +0 -134
  37. package/tsconfig.build.json +0 -9
  38. package/tsconfig.build.tsbuildinfo +0 -1
  39. package/tsconfig.json +0 -7
  40. package/tsconfig.tests.json +0 -7
@@ -1,126 +0,0 @@
1
- import { BlobPermission } from './blob-permission.js'
2
-
3
- describe('BlobPermission', () => {
4
- describe('static', () => {
5
- describe('fromString', () => {
6
- it('should parse positional scope', () => {
7
- const scope = BlobPermission.fromString('blob:image/png')
8
- expect(scope).not.toBeNull()
9
- expect(scope!.accept).toEqual(['image/png'])
10
- })
11
-
12
- it('should parse valid blob scope with multiple accept parameters', () => {
13
- const scope = BlobPermission.fromString(
14
- 'blob?accept=image/png&accept=image/jpeg',
15
- )
16
- expect(scope).not.toBeNull()
17
- expect(scope!.accept).toEqual(['image/png', 'image/jpeg'])
18
- })
19
-
20
- it('should reject blob scope without accept', () => {
21
- const scope = BlobPermission.fromString('blob')
22
- expect(scope).toBeNull()
23
- })
24
-
25
- for (const invalid of [
26
- 'invalid',
27
- 'scope',
28
- 'blob:invalid',
29
- 'blob?accept=invalid-mime',
30
- 'blob?accept=invalid',
31
- 'blob:*/**',
32
- 'blob:*/png',
33
- ]) {
34
- it(`should return null for invalid rpc scope: ${invalid}`, () => {
35
- expect(BlobPermission.fromString(invalid)).toBeNull()
36
- })
37
- }
38
- })
39
-
40
- describe('scopeNeededFor', () => {
41
- it('should return correct scope string for specific MIME type', () => {
42
- const scope = BlobPermission.scopeNeededFor({ mime: 'image/png' })
43
- expect(scope).toBe('blob:image/png')
44
- })
45
-
46
- it('should return scope that accepts all MIME types', () => {
47
- const scope = BlobPermission.scopeNeededFor({
48
- mime: 'application/json',
49
- })
50
- expect(scope).toBe('blob:application/json')
51
- })
52
- })
53
- })
54
-
55
- describe('instance', () => {
56
- describe('matches', () => {
57
- it('should match exact MIME type', () => {
58
- const scope = BlobPermission.fromString('blob:image/png')
59
- expect(scope).not.toBeNull()
60
- expect(scope!.matches({ mime: 'image/png' })).toBe(true)
61
- })
62
-
63
- it('should match wildcard MIME type', () => {
64
- const scope = BlobPermission.fromString('blob:*/*')
65
- expect(scope).not.toBeNull()
66
- expect(scope!.matches({ mime: 'image/jpeg' })).toBe(true)
67
- expect(scope!.matches({ mime: 'application/json' })).toBe(true)
68
- })
69
-
70
- it('should match subtype wildcard MIME type', () => {
71
- const scope = BlobPermission.fromString('blob:image/*')
72
- expect(scope).not.toBeNull()
73
- expect(scope!.matches({ mime: 'image/gif' })).toBe(true)
74
- })
75
-
76
- it('should not match different MIME type', () => {
77
- const scope = BlobPermission.fromString('blob:image/png')
78
- expect(scope).not.toBeNull()
79
- expect(scope!.matches({ mime: 'image/jpeg' })).toBe(false)
80
- })
81
-
82
- it('should match multiple accept values', () => {
83
- const scope = BlobPermission.fromString(
84
- 'blob?accept=image/png&accept=image/jpeg',
85
- )
86
- expect(scope).not.toBeNull()
87
- expect(scope!.matches({ mime: 'image/png' })).toBe(true)
88
- expect(scope!.matches({ mime: 'image/jpeg' })).toBe(true)
89
- expect(scope!.matches({ mime: 'image/gif' })).toBe(false)
90
- })
91
- })
92
-
93
- describe('toString', () => {
94
- it('should format scope with accept parameter', () => {
95
- const scope = new BlobPermission(['image/png', 'image/jpeg'])
96
- expect(scope.toString()).toBe('blob?accept=image/jpeg&accept=image/png')
97
- })
98
-
99
- it('should strip redundant accept parameters', () => {
100
- expect(new BlobPermission(['*/*', 'image/*']).toString()).toBe(
101
- 'blob:*/*',
102
- )
103
- expect(new BlobPermission(['*/*', 'image/png']).toString()).toBe(
104
- 'blob:*/*',
105
- )
106
- expect(new BlobPermission(['image/*', 'image/png']).toString()).toBe(
107
- 'blob:image/*',
108
- )
109
- })
110
-
111
- it('should use positional format for single accept', () => {
112
- expect(new BlobPermission(['image/png']).toString()).toBe(
113
- 'blob:image/png',
114
- )
115
- expect(new BlobPermission(['image/*']).toString()).toBe('blob:image/*')
116
- expect(new BlobPermission(['*/*']).toString()).toBe('blob:*/*')
117
- })
118
-
119
- it('should use query format for multiple accepts', () => {
120
- expect(new BlobPermission(['image/png', 'image/jpeg']).toString()).toBe(
121
- 'blob?accept=image/jpeg&accept=image/png',
122
- )
123
- })
124
- })
125
- })
126
- })
@@ -1,105 +0,0 @@
1
- import { Accept, isAccept, matchesAnyAccept } from '../lib/mime.js'
2
- import { Parser } from '../lib/parser.js'
3
- import { ResourcePermission } from '../lib/resource-permission.js'
4
- import { ScopeStringSyntax } from '../lib/syntax-string.js'
5
- import {
6
- NeArray,
7
- NeRoArray,
8
- ParamValue,
9
- ScopeSyntax,
10
- isScopeStringFor,
11
- } from '../lib/syntax.js'
12
-
13
- export { type Accept }
14
-
15
- export const DEFAULT_ACCEPT = Object.freeze(['*/*'] as const)
16
-
17
- export type BlobPermissionMatch = {
18
- mime: string
19
- }
20
-
21
- export class BlobPermission
22
- implements ResourcePermission<'blob', BlobPermissionMatch>
23
- {
24
- constructor(public readonly accept: NeRoArray<Accept>) {}
25
-
26
- matches(options: BlobPermissionMatch) {
27
- return matchesAnyAccept(this.accept, options.mime)
28
- }
29
-
30
- toString() {
31
- return BlobPermission.parser.format(this)
32
- }
33
-
34
- protected static readonly parser = new Parser(
35
- 'blob',
36
- {
37
- accept: {
38
- multiple: true,
39
- required: true,
40
- validate: isAccept,
41
- normalize: (value) => {
42
- // Returns a more concise representation of the accept values.
43
- if (value.includes('*/*')) return DEFAULT_ACCEPT
44
-
45
- return value
46
- .map(toLowerCase)
47
- .filter(isNonRedundant)
48
- .sort() as NeArray<Accept>
49
- },
50
- },
51
- },
52
- 'accept',
53
- )
54
-
55
- static fromString(scope: string) {
56
- if (!isScopeStringFor(scope, 'blob')) return null
57
- const syntax = ScopeStringSyntax.fromString(scope)
58
- return BlobPermission.fromSyntax(syntax)
59
- }
60
-
61
- static fromSyntax(syntax: ScopeSyntax<'blob'>) {
62
- const result = BlobPermission.parser.parse(syntax)
63
- if (!result) return null
64
-
65
- return new BlobPermission(result.accept)
66
- }
67
-
68
- static scopeNeededFor(options: BlobPermissionMatch) {
69
- return BlobPermission.parser.format({
70
- accept: [options.mime as Accept],
71
- })
72
- }
73
- }
74
-
75
- function toLowerCase<T extends ParamValue>(
76
- value: T,
77
- ): T extends string ? string : T {
78
- return (
79
- typeof value === 'string' ? value.toLowerCase() : value
80
- ) as T extends string ? string : T
81
- }
82
-
83
- function isNonRedundant(
84
- value: ParamValue,
85
- index: number,
86
- arr: readonly ParamValue[],
87
- ): boolean {
88
- if (typeof value !== 'string') {
89
- return true
90
- }
91
- if (value.endsWith('/*')) {
92
- // assuming the array contains unique element, wildcards cannot be redundant
93
- // with one another ('image/*' is not redundant with 'text/*')
94
- return true
95
- }
96
- const base = value.split('/', 1)[0]
97
- if (arr.includes(`${base}/*`)) {
98
- // If another value in the array is a wildcard for the same base, we can
99
- // skip this one as it is redundant. e.g. if the array contains 'image/png'
100
- // and 'image/*', we can skip 'image/png' because 'image/*' already covers
101
- // it.
102
- return false
103
- }
104
- return true
105
- }
@@ -1,80 +0,0 @@
1
- import { IdentityPermission } from './identity-permission.js'
2
-
3
- describe('IdentityPermission', () => {
4
- describe('static', () => {
5
- describe('fromString', () => {
6
- it('should parse positional scope', () => {
7
- const scope = IdentityPermission.fromString('identity:handle')
8
- expect(scope).not.toBeNull()
9
- expect(scope!.attr).toBe('handle')
10
- })
11
-
12
- it('should parse valid identity scope with wildcard attribute', () => {
13
- const scope = IdentityPermission.fromString('identity:*')
14
- expect(scope).not.toBeNull()
15
- expect(scope!.attr).toBe('*')
16
- })
17
-
18
- it('should return null for invalid identity scope', () => {
19
- expect(IdentityPermission.fromString('invalid')).toBeNull()
20
- expect(IdentityPermission.fromString('identity:invalid')).toBeNull()
21
- })
22
-
23
- for (const invalid of [
24
- 'identity:*?action=*',
25
- 'identity:*?action=manage',
26
- 'identity:*?action=submit',
27
- 'invalid',
28
- 'identity:invalid',
29
- 'identity:handle?action=invalid',
30
- 'identity?attribute=invalid&action=invalid',
31
- ]) {
32
- it(`should return null for invalid rpc scope: ${invalid}`, () => {
33
- expect(IdentityPermission.fromString(invalid)).toBeNull()
34
- })
35
- }
36
- })
37
-
38
- describe('scopeNeededFor', () => {
39
- it('should return correct scope string for specific attribute and action', () => {
40
- const scope = IdentityPermission.scopeNeededFor({ attr: 'handle' })
41
- expect(scope).toBe('identity:handle')
42
- })
43
-
44
- it('should return scope that accepts all attributes with specific action', () => {
45
- const scope = IdentityPermission.scopeNeededFor({ attr: '*' })
46
- expect(scope).toBe('identity:*')
47
- })
48
- })
49
- })
50
-
51
- describe('instance', () => {
52
- describe('matches', () => {
53
- it('should match default attribute and action', () => {
54
- const scope = IdentityPermission.fromString('identity:handle')
55
- expect(scope).not.toBeNull()
56
- expect(scope!.matches({ attr: 'handle' })).toBe(true)
57
- expect(scope!.matches({ attr: '*' })).toBe(false)
58
- })
59
-
60
- it('should match wildcard attribute with specific action', () => {
61
- const scope = IdentityPermission.fromString('identity:*')
62
- expect(scope).not.toBeNull()
63
- expect(scope!.matches({ attr: '*' })).toBe(true)
64
- expect(scope!.matches({ attr: 'handle' })).toBe(true)
65
- })
66
- })
67
-
68
- describe('toString', () => {
69
- it('should format scope with default action', () => {
70
- const scope = new IdentityPermission('handle')
71
- expect(scope.toString()).toBe('identity:handle')
72
- })
73
-
74
- it('should format wildcard attribute with default action', () => {
75
- const scope = new IdentityPermission('*')
76
- expect(scope.toString()).toBe('identity:*')
77
- })
78
- })
79
- })
80
- })
@@ -1,54 +0,0 @@
1
- import { Parser } from '../lib/parser.js'
2
- import { ResourcePermission } from '../lib/resource-permission.js'
3
- import { ScopeStringSyntax } from '../lib/syntax-string.js'
4
- import { ScopeSyntax, isScopeStringFor } from '../lib/syntax.js'
5
- import { knownValuesValidator } from '../lib/util.js'
6
-
7
- export const IDENTITY_ATTRIBUTES = Object.freeze(['handle', '*'] as const)
8
- export type IdentityAttribute = (typeof IDENTITY_ATTRIBUTES)[number]
9
-
10
- export type IdentityPermissionMatch = {
11
- attr: IdentityAttribute
12
- }
13
-
14
- export class IdentityPermission
15
- implements ResourcePermission<'identity', IdentityPermissionMatch>
16
- {
17
- constructor(public readonly attr: IdentityAttribute) {}
18
-
19
- matches(options: IdentityPermissionMatch) {
20
- return this.attr === '*' || this.attr === options.attr
21
- }
22
-
23
- toString() {
24
- return IdentityPermission.parser.format(this)
25
- }
26
-
27
- protected static readonly parser = new Parser(
28
- 'identity',
29
- {
30
- attr: {
31
- multiple: false,
32
- required: true,
33
- validate: knownValuesValidator(IDENTITY_ATTRIBUTES),
34
- },
35
- },
36
- 'attr',
37
- )
38
-
39
- static fromString(scope: string) {
40
- if (!isScopeStringFor(scope, 'identity')) return null
41
- const syntax = ScopeStringSyntax.fromString(scope)
42
- return IdentityPermission.fromSyntax(syntax)
43
- }
44
-
45
- static fromSyntax(syntax: ScopeSyntax<'identity'>) {
46
- const result = IdentityPermission.parser.parse(syntax)
47
- if (!result) return null
48
- return new IdentityPermission(result.attr)
49
- }
50
-
51
- static scopeNeededFor(options: IdentityPermissionMatch): string {
52
- return IdentityPermission.parser.format(options)
53
- }
54
- }