@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/atproto-oauth-scope.d.ts +12 -0
  3. package/dist/atproto-oauth-scope.d.ts.map +1 -0
  4. package/dist/atproto-oauth-scope.js +32 -0
  5. package/dist/atproto-oauth-scope.js.map +1 -0
  6. package/dist/index.d.ts +9 -13
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +9 -13
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/lexicon.d.ts +2 -0
  11. package/dist/lib/lexicon.d.ts.map +1 -0
  12. package/dist/lib/lexicon.js +3 -0
  13. package/dist/lib/lexicon.js.map +1 -0
  14. package/dist/lib/mime.d.ts +1 -1
  15. package/dist/lib/mime.d.ts.map +1 -1
  16. package/dist/lib/mime.js +2 -0
  17. package/dist/lib/mime.js.map +1 -1
  18. package/dist/lib/nsid.d.ts +2 -2
  19. package/dist/lib/nsid.d.ts.map +1 -1
  20. package/dist/lib/nsid.js +4 -6
  21. package/dist/lib/nsid.js.map +1 -1
  22. package/dist/lib/parser.d.ts +29 -0
  23. package/dist/lib/parser.d.ts.map +1 -0
  24. package/dist/lib/parser.js +152 -0
  25. package/dist/lib/parser.js.map +1 -0
  26. package/dist/lib/resource-permission.d.ts +10 -0
  27. package/dist/lib/resource-permission.d.ts.map +1 -0
  28. package/dist/lib/resource-permission.js +3 -0
  29. package/dist/lib/resource-permission.js.map +1 -0
  30. package/dist/lib/syntax-lexicon.d.ts +26 -0
  31. package/dist/lib/syntax-lexicon.d.ts.map +1 -0
  32. package/dist/lib/syntax-lexicon.js +58 -0
  33. package/dist/lib/syntax-lexicon.js.map +1 -0
  34. package/dist/lib/syntax-string.d.ts +16 -0
  35. package/dist/lib/syntax-string.d.ts.map +1 -0
  36. package/dist/lib/syntax-string.js +121 -0
  37. package/dist/lib/syntax-string.js.map +1 -0
  38. package/dist/lib/syntax.d.ts +23 -0
  39. package/dist/lib/syntax.d.ts.map +1 -0
  40. package/dist/lib/syntax.js +22 -0
  41. package/dist/lib/syntax.js.map +1 -0
  42. package/dist/lib/util.d.ts +4 -1
  43. package/dist/lib/util.d.ts.map +1 -1
  44. package/dist/lib/util.js +4 -12
  45. package/dist/lib/util.js.map +1 -1
  46. package/dist/scope-permissions-transition.d.ts +15 -0
  47. package/dist/scope-permissions-transition.d.ts.map +1 -0
  48. package/dist/{permission-set-transition.js → scope-permissions-transition.js} +8 -6
  49. package/dist/scope-permissions-transition.js.map +1 -0
  50. package/dist/scope-permissions.d.ts +22 -0
  51. package/dist/scope-permissions.d.ts.map +1 -0
  52. package/dist/{permission-set.js → scope-permissions.js} +20 -16
  53. package/dist/scope-permissions.js.map +1 -0
  54. package/dist/scopes/account-permission.d.ts +35 -0
  55. package/dist/scopes/account-permission.d.ts.map +1 -0
  56. package/dist/scopes/account-permission.js +71 -0
  57. package/dist/scopes/account-permission.js.map +1 -0
  58. package/dist/scopes/blob-permission.d.ts +27 -0
  59. package/dist/scopes/blob-permission.d.ts.map +1 -0
  60. package/dist/scopes/blob-permission.js +86 -0
  61. package/dist/scopes/blob-permission.js.map +1 -0
  62. package/dist/scopes/identity-permission.d.ts +25 -0
  63. package/dist/scopes/identity-permission.d.ts.map +1 -0
  64. package/dist/scopes/identity-permission.js +53 -0
  65. package/dist/scopes/identity-permission.js.map +1 -0
  66. package/dist/scopes/include-scope.d.ts +54 -0
  67. package/dist/scopes/include-scope.d.ts.map +1 -0
  68. package/dist/scopes/include-scope.js +156 -0
  69. package/dist/scopes/include-scope.js.map +1 -0
  70. package/dist/scopes/repo-permission.d.ts +40 -0
  71. package/dist/scopes/repo-permission.d.ts.map +1 -0
  72. package/dist/scopes/repo-permission.js +101 -0
  73. package/dist/scopes/repo-permission.js.map +1 -0
  74. package/dist/scopes/rpc-permission.d.ts +38 -0
  75. package/dist/scopes/rpc-permission.d.ts.map +1 -0
  76. package/dist/scopes/rpc-permission.js +81 -0
  77. package/dist/scopes/rpc-permission.js.map +1 -0
  78. package/dist/scopes-set.d.ts +12 -1
  79. package/dist/scopes-set.d.ts.map +1 -1
  80. package/dist/scopes-set.js +49 -3
  81. package/dist/scopes-set.js.map +1 -1
  82. package/package.json +7 -3
  83. package/src/atproto-oauth-scope.ts +43 -0
  84. package/src/index.ts +10 -14
  85. package/src/lib/lexicon.ts +1 -0
  86. package/src/lib/mime.ts +2 -1
  87. package/src/lib/nsid.ts +5 -6
  88. package/src/lib/parser.ts +176 -0
  89. package/src/lib/resource-permission.ts +10 -0
  90. package/src/lib/syntax-lexicon.ts +55 -0
  91. package/src/lib/syntax-string.test.ts +130 -0
  92. package/src/lib/syntax-string.ts +132 -0
  93. package/src/lib/syntax.test.ts +43 -0
  94. package/src/lib/syntax.ts +47 -0
  95. package/src/lib/util.ts +7 -12
  96. package/src/{permission-set-transition.test.ts → scope-permissions-transition.test.ts} +34 -21
  97. package/src/{permission-set-transition.ts → scope-permissions-transition.ts} +16 -12
  98. package/src/{permission-set.test.ts → scope-permissions.test.ts} +77 -35
  99. package/src/scope-permissions.ts +91 -0
  100. package/src/{resources/account-scope.test.ts → scopes/account-permission.test.ts} +45 -33
  101. package/src/scopes/account-permission.ts +75 -0
  102. package/src/{resources/blob-scope.test.ts → scopes/blob-permission.test.ts} +31 -23
  103. package/src/scopes/blob-permission.ts +105 -0
  104. package/src/{resources/identity-scope.test.ts → scopes/identity-permission.test.ts} +13 -13
  105. package/src/scopes/identity-permission.ts +54 -0
  106. package/src/scopes/include-scope.test.ts +626 -0
  107. package/src/scopes/include-scope.ts +168 -0
  108. package/src/{resources/repo-scope.test.ts → scopes/repo-permission.test.ts} +77 -65
  109. package/src/scopes/repo-permission.ts +111 -0
  110. package/src/scopes/rpc-permission.test.ts +323 -0
  111. package/src/scopes/rpc-permission.ts +85 -0
  112. package/src/scopes-set.test.ts +5 -5
  113. package/src/scopes-set.ts +79 -5
  114. package/tsconfig.build.tsbuildinfo +1 -1
  115. package/tsconfig.tests.tsbuildinfo +1 -1
  116. package/dist/lib/did.d.ts +0 -3
  117. package/dist/lib/did.d.ts.map +0 -1
  118. package/dist/lib/did.js +0 -6
  119. package/dist/lib/did.js.map +0 -1
  120. package/dist/parser.d.ts +0 -31
  121. package/dist/parser.d.ts.map +0 -1
  122. package/dist/parser.js +0 -118
  123. package/dist/parser.js.map +0 -1
  124. package/dist/permission-set-transition.d.ts +0 -15
  125. package/dist/permission-set-transition.d.ts.map +0 -1
  126. package/dist/permission-set-transition.js.map +0 -1
  127. package/dist/permission-set.d.ts +0 -22
  128. package/dist/permission-set.d.ts.map +0 -1
  129. package/dist/permission-set.js.map +0 -1
  130. package/dist/resources/account-scope.d.ts +0 -35
  131. package/dist/resources/account-scope.d.ts.map +0 -1
  132. package/dist/resources/account-scope.js +0 -60
  133. package/dist/resources/account-scope.js.map +0 -1
  134. package/dist/resources/blob-scope.d.ts +0 -25
  135. package/dist/resources/blob-scope.d.ts.map +0 -1
  136. package/dist/resources/blob-scope.js +0 -74
  137. package/dist/resources/blob-scope.js.map +0 -1
  138. package/dist/resources/identity-scope.d.ts +0 -25
  139. package/dist/resources/identity-scope.d.ts.map +0 -1
  140. package/dist/resources/identity-scope.js +0 -46
  141. package/dist/resources/identity-scope.js.map +0 -1
  142. package/dist/resources/repo-scope.d.ts +0 -37
  143. package/dist/resources/repo-scope.d.ts.map +0 -1
  144. package/dist/resources/repo-scope.js +0 -92
  145. package/dist/resources/repo-scope.js.map +0 -1
  146. package/dist/resources/rpc-scope.d.ts +0 -31
  147. package/dist/resources/rpc-scope.d.ts.map +0 -1
  148. package/dist/resources/rpc-scope.js +0 -74
  149. package/dist/resources/rpc-scope.js.map +0 -1
  150. package/dist/syntax.d.ts +0 -76
  151. package/dist/syntax.d.ts.map +0 -1
  152. package/dist/syntax.js +0 -249
  153. package/dist/syntax.js.map +0 -1
  154. package/dist/utilities.d.ts +0 -17
  155. package/dist/utilities.d.ts.map +0 -1
  156. package/dist/utilities.js +0 -108
  157. package/dist/utilities.js.map +0 -1
  158. package/src/lib/did.ts +0 -3
  159. package/src/parser.ts +0 -150
  160. package/src/permission-set.ts +0 -78
  161. package/src/resources/account-scope.ts +0 -66
  162. package/src/resources/blob-scope.ts +0 -86
  163. package/src/resources/identity-scope.ts +0 -49
  164. package/src/resources/repo-scope.ts +0 -101
  165. package/src/resources/rpc-scope.test.ts +0 -280
  166. package/src/resources/rpc-scope.ts +0 -77
  167. package/src/syntax.test.ts +0 -203
  168. package/src/syntax.ts +0 -325
  169. 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 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
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 { PermissionSetTransition } from './permission-set-transition.js'
1
+ import { ScopePermissionsTransition } from './scope-permissions-transition.js'
2
2
 
3
- describe('PermissionSetTransition', () => {
3
+ describe('ScopePermissionsTransition', () => {
4
4
  describe('allowsAccount', () => {
5
5
  it('should allow account:email with transition:email', () => {
6
- const set = new PermissionSetTransition('transition:email account:repo')
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(true)
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 PermissionSetTransition('transition:generic')
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 PermissionSetTransition('transition:generic')
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 PermissionSetTransition('transition:generic')
49
+ const set = new ScopePermissionsTransition('transition:generic')
48
50
  expect(
49
- set.allowsRpc({ aud: 'did:example:123', lxm: 'app.bsky.feed.post' }),
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:123', lxm: 'com.example.foo' }),
57
+ set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
53
58
  ).toBe(true)
54
- expect(set.allowsRpc({ aud: 'did:example:123', lxm: '*' })).toBe(true)
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 PermissionSetTransition('transition:chat.bsky')
63
+ const set = new ScopePermissionsTransition('transition:chat.bsky')
59
64
  expect(
60
65
  set.allowsRpc({
61
- aud: 'did:example:123',
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:123',
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({ aud: 'did:example:123', lxm: 'app.bsky.feed.post' }),
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:123', lxm: 'com.example.foo' }),
86
+ set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
79
87
  ).toBe(false)
80
- expect(set.allowsRpc({ aud: 'did:example:123', lxm: '*' })).toBe(false)
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 PermissionSetTransition('transition:generic')
94
+ const set = new ScopePermissionsTransition('transition:generic')
85
95
 
86
96
  expect(
87
97
  set.allowsRpc({
88
- aud: 'did:example:123',
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:123',
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({ aud: 'did:example:123', lxm: 'app.bsky.feed.post' }),
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:123', lxm: 'com.example.foo' }),
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
- AccountScopeMatch,
3
- BlobScopeMatch,
4
- PermissionSet,
5
- RepoScopeMatch,
6
- RpcScopeMatch,
7
- } from './permission-set.js'
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 PermissionSetTransition extends PermissionSet {
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: AccountScopeMatch): boolean {
27
- if (options.attr === 'email' && this.hasTransitionEmail) {
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: BlobScopeMatch): boolean {
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: RepoScopeMatch): boolean {
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: RpcScopeMatch) {
54
+ override allowsRpc(options: RpcPermissionMatch) {
51
55
  const { lxm } = options
52
56
 
53
57
  if (this.hasTransitionGeneric && lxm === '*') {