@atproto/oauth-scopes 0.5.1 → 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 +22 -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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @atproto/oauth-scopes
|
|
2
2
|
|
|
3
|
+
## 0.5.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update TypeScript build to rely on references to composite internal projects
|
|
8
|
+
|
|
9
|
+
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bundle only necessary files in the NPM tarball, including the `CHANGELOG.md` and `README.md` files (if present).
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`28a0b58`](https://github.com/bluesky-social/atproto/commit/28a0b588147863eaef948cd2bb8fc0f19d08cda9), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07)]:
|
|
12
|
+
- @atproto/syntax@0.6.4
|
|
13
|
+
- @atproto/did@0.5.3
|
|
14
|
+
|
|
15
|
+
## 0.5.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#5151](https://github.com/bluesky-social/atproto/pull/5151) [`a51c45d`](https://github.com/bluesky-social/atproto/commit/a51c45d38f6bd7b8765f640e564cf921d52162e7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update dependencies
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [[`a51c45d`](https://github.com/bluesky-social/atproto/commit/a51c45d38f6bd7b8765f640e564cf921d52162e7)]:
|
|
22
|
+
- @atproto/did@0.5.2
|
|
23
|
+
- @atproto/syntax@0.6.3
|
|
24
|
+
|
|
3
25
|
## 0.5.1
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/oauth-scopes",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"engines": {
|
|
5
|
-
"node": ">=22"
|
|
6
|
-
},
|
|
3
|
+
"version": "0.5.3",
|
|
7
4
|
"license": "MIT",
|
|
8
5
|
"description": "A library for manipulating and validating ATproto OAuth scopes in TypeScript.",
|
|
9
6
|
"keywords": [
|
|
@@ -18,6 +15,10 @@
|
|
|
18
15
|
"url": "https://github.com/bluesky-social/atproto",
|
|
19
16
|
"directory": "packages/oauth/auth-scopes"
|
|
20
17
|
},
|
|
18
|
+
"files": [
|
|
19
|
+
"./dist",
|
|
20
|
+
"./CHANGELOG.md"
|
|
21
|
+
],
|
|
21
22
|
"type": "module",
|
|
22
23
|
"exports": {
|
|
23
24
|
".": {
|
|
@@ -25,9 +26,12 @@
|
|
|
25
26
|
"default": "./dist/index.js"
|
|
26
27
|
}
|
|
27
28
|
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
},
|
|
28
32
|
"dependencies": {
|
|
29
|
-
"@atproto/did": "^0.5.
|
|
30
|
-
"@atproto/syntax": "^0.6.
|
|
33
|
+
"@atproto/did": "^0.5.3",
|
|
34
|
+
"@atproto/syntax": "^0.6.4"
|
|
31
35
|
},
|
|
32
36
|
"devDependencies": {
|
|
33
37
|
"jest": "^30.0.0"
|
package/jest.config.cjs
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/** @type {import('jest').Config} */
|
|
2
|
-
module.exports = {
|
|
3
|
-
displayName: 'OAuth Scopes',
|
|
4
|
-
transform: {
|
|
5
|
-
'^.+\\.(t|j)s$': [
|
|
6
|
-
'@swc/jest',
|
|
7
|
-
{ jsc: { transform: {} }, module: { type: 'es6' } },
|
|
8
|
-
],
|
|
9
|
-
},
|
|
10
|
-
extensionsToTreatAsEsm: ['.ts'],
|
|
11
|
-
transformIgnorePatterns: [],
|
|
12
|
-
setupFiles: ['<rootDir>/../../../test.setup.ts'],
|
|
13
|
-
moduleNameMapper: { '^(\\.\\.?\\/.+)\\.js$': ['$1.ts', '$1.js'] },
|
|
14
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { ScopeStringFor, isScopeStringFor } from './lib/syntax.js'
|
|
2
|
-
import { isNonNullable } from './lib/util.js'
|
|
3
|
-
import { AccountPermission } from './scopes/account-permission.js'
|
|
4
|
-
import { BlobPermission } from './scopes/blob-permission.js'
|
|
5
|
-
import { IdentityPermission } from './scopes/identity-permission.js'
|
|
6
|
-
import { IncludeScope } from './scopes/include-scope.js'
|
|
7
|
-
import { RepoPermission } from './scopes/repo-permission.js'
|
|
8
|
-
import { RpcPermission } from './scopes/rpc-permission.js'
|
|
9
|
-
|
|
10
|
-
export { type ScopeStringFor, isScopeStringFor }
|
|
11
|
-
|
|
12
|
-
export const STATIC_SCOPE_VALUES = Object.freeze([
|
|
13
|
-
'atproto',
|
|
14
|
-
'transition:email',
|
|
15
|
-
'transition:generic',
|
|
16
|
-
'transition:chat.bsky',
|
|
17
|
-
] as const)
|
|
18
|
-
|
|
19
|
-
export type StaticScopeValue = (typeof STATIC_SCOPE_VALUES)[number]
|
|
20
|
-
export function isStaticScopeValue(value: string): value is StaticScopeValue {
|
|
21
|
-
return (STATIC_SCOPE_VALUES as readonly string[]).includes(value)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type AtprotoOauthScope =
|
|
25
|
-
| StaticScopeValue
|
|
26
|
-
| ScopeStringFor<'account'>
|
|
27
|
-
| ScopeStringFor<'blob'>
|
|
28
|
-
| ScopeStringFor<'identity'>
|
|
29
|
-
| ScopeStringFor<'include'>
|
|
30
|
-
| ScopeStringFor<'repo'>
|
|
31
|
-
| ScopeStringFor<'rpc'>
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @note This function does not only verify the scope string format (with
|
|
35
|
-
* {@link isScopeStringFor}), but also checks if the provided parameters are
|
|
36
|
-
* valid according to the respective scope syntax definition. This allows
|
|
37
|
-
* excluding scopes that cannot be fully interpreted by the current version of
|
|
38
|
-
* the code.
|
|
39
|
-
*/
|
|
40
|
-
export function isAtprotoOauthScope(value: string): value is AtprotoOauthScope {
|
|
41
|
-
return (
|
|
42
|
-
isStaticScopeValue(value) ||
|
|
43
|
-
AccountPermission.fromString(value) != null ||
|
|
44
|
-
BlobPermission.fromString(value) != null ||
|
|
45
|
-
IdentityPermission.fromString(value) != null ||
|
|
46
|
-
IncludeScope.fromString(value) != null ||
|
|
47
|
-
RepoPermission.fromString(value) != null ||
|
|
48
|
-
RpcPermission.fromString(value) != null
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function normalizeAtprotoOauthScope(scope: string) {
|
|
53
|
-
return scope
|
|
54
|
-
.split(' ')
|
|
55
|
-
.map(normalizeAtprotoOauthScopeValue)
|
|
56
|
-
.filter(isNonNullable)
|
|
57
|
-
.sort()
|
|
58
|
-
.join(' ')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function normalizeAtprotoOauthScopeValue(
|
|
62
|
-
value: string,
|
|
63
|
-
): AtprotoOauthScope | null {
|
|
64
|
-
if (isStaticScopeValue(value)) return value
|
|
65
|
-
|
|
66
|
-
for (const Scope of [
|
|
67
|
-
AccountPermission,
|
|
68
|
-
BlobPermission,
|
|
69
|
-
IdentityPermission,
|
|
70
|
-
IncludeScope,
|
|
71
|
-
RepoPermission,
|
|
72
|
-
RpcPermission,
|
|
73
|
-
]) {
|
|
74
|
-
const parsed = Scope.fromString(value)
|
|
75
|
-
if (parsed) return parsed.toString()
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return null
|
|
79
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export * from './atproto-oauth-scope.js'
|
|
2
|
-
|
|
3
|
-
export * from './scope-missing-error.js'
|
|
4
|
-
export * from './scope-permissions-transition.js'
|
|
5
|
-
export * from './scope-permissions.js'
|
|
6
|
-
export * from './scopes-set.js'
|
|
7
|
-
|
|
8
|
-
export * from './scopes/account-permission.js'
|
|
9
|
-
export * from './scopes/blob-permission.js'
|
|
10
|
-
export * from './scopes/identity-permission.js'
|
|
11
|
-
export * from './scopes/include-scope.js'
|
|
12
|
-
export * from './scopes/repo-permission.js'
|
|
13
|
-
export * from './scopes/rpc-permission.js'
|
package/src/lib/lexicon.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { ParamValue } from './syntax.js'
|
|
2
|
-
|
|
3
|
-
// @NOTE Not types from from '@atproto/lex-document' because we want a readonly
|
|
4
|
-
// version here to prevent accidental mutation.
|
|
5
|
-
|
|
6
|
-
export type LexiconPermission<P extends string = string> = {
|
|
7
|
-
readonly type: 'permission'
|
|
8
|
-
readonly resource: P
|
|
9
|
-
readonly [x: string]: undefined | ParamValue | readonly ParamValue[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type LangMap = { readonly [Lang in string]?: string }
|
|
13
|
-
|
|
14
|
-
export type LexiconPermissionSet = {
|
|
15
|
-
readonly type: 'permission-set'
|
|
16
|
-
readonly permissions: readonly LexiconPermission<string>[]
|
|
17
|
-
readonly title?: string
|
|
18
|
-
readonly 'title:lang'?: LangMap
|
|
19
|
-
readonly detail?: string
|
|
20
|
-
readonly 'detail:lang'?: LangMap
|
|
21
|
-
}
|
package/src/lib/mime.test.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { isAccept, isMime, matchesAccept, matchesAnyAccept } from './mime.js'
|
|
2
|
-
|
|
3
|
-
describe('isAccept', () => {
|
|
4
|
-
it('should return true for valid MIME types', () => {
|
|
5
|
-
expect(isAccept('image/png')).toBe(true)
|
|
6
|
-
expect(isAccept('application/json')).toBe(true)
|
|
7
|
-
expect(isAccept('text/html')).toBe(true)
|
|
8
|
-
expect(isAccept('image/*')).toBe(true)
|
|
9
|
-
expect(isAccept('*/*')).toBe(true)
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('should return false for invalid MIME types', () => {
|
|
13
|
-
expect(isAccept('image//png')).toBe(false)
|
|
14
|
-
expect(isAccept('/png')).toBe(false)
|
|
15
|
-
expect(isAccept('image/')).toBe(false)
|
|
16
|
-
expect(isAccept('image/**')).toBe(false)
|
|
17
|
-
expect(isAccept('*/png')).toBe(false)
|
|
18
|
-
expect(isAccept('*')).toBe(false)
|
|
19
|
-
expect(isAccept('image/png/extra')).toBe(false)
|
|
20
|
-
})
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
describe('isMime', () => {
|
|
24
|
-
it('should return true for valid MIME types', () => {
|
|
25
|
-
expect(isMime('image/png')).toBe(true)
|
|
26
|
-
expect(isMime('application/json')).toBe(true)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('should return false for invalid MIME types', () => {
|
|
30
|
-
expect(isMime('image/*')).toBe(false)
|
|
31
|
-
expect(isMime('*/*')).toBe(false)
|
|
32
|
-
expect(isMime('image/png/extra')).toBe(false)
|
|
33
|
-
expect(isMime('*/mime')).toBe(false)
|
|
34
|
-
expect(isMime('/png')).toBe(false)
|
|
35
|
-
expect(isMime('image/')).toBe(false)
|
|
36
|
-
expect(isMime('image')).toBe(false)
|
|
37
|
-
expect(isMime('image/ png')).toBe(false)
|
|
38
|
-
expect(isMime('image//png')).toBe(false)
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('matchesAccept', () => {
|
|
43
|
-
it('should match exact MIME type', () => {
|
|
44
|
-
expect(matchesAccept('image/png', 'image/png')).toBe(true)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('should match wildcard MIME type', () => {
|
|
48
|
-
expect(matchesAccept('image/*', 'image/jpeg')).toBe(true)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('should match subtype wildcard MIME type', () => {
|
|
52
|
-
expect(matchesAccept('image/*', 'image/gif')).toBe(true)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('should not match different MIME type', () => {
|
|
56
|
-
expect(matchesAccept('image/png', 'image/jpeg')).toBe(false)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('should not match different wildcard MIME type', () => {
|
|
60
|
-
expect(matchesAccept('image/*', 'text/html')).toBe(false)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('should match any MIME type with *', () => {
|
|
64
|
-
expect(matchesAccept('*/*', 'application/json')).toBe(true)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('should not match invalid MIME type', () => {
|
|
68
|
-
expect(matchesAccept('image/png', '*/mime')).toBe(false)
|
|
69
|
-
expect(matchesAccept('image/png', 'image')).toBe(false)
|
|
70
|
-
expect(matchesAccept('image/*', 'image//png')).toBe(false)
|
|
71
|
-
expect(matchesAccept('image/*', 'image/ png')).toBe(false)
|
|
72
|
-
expect(matchesAccept('*/*', 'image/')).toBe(false)
|
|
73
|
-
expect(matchesAccept('*/*', '/mime')).toBe(false)
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
describe('matchesAnyAccept', () => {
|
|
78
|
-
it('should return true if any accept matches', () => {
|
|
79
|
-
const accepts = ['image/png', 'application/json'] as const
|
|
80
|
-
expect(matchesAnyAccept(accepts, 'image/png')).toBe(true)
|
|
81
|
-
expect(matchesAnyAccept(accepts, 'application/json')).toBe(true)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('should return false if no accept matches', () => {
|
|
85
|
-
const accepts = ['image/png', 'application/json'] as const
|
|
86
|
-
expect(matchesAnyAccept(accepts, 'text/html')).toBe(false)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('should handle empty accepts array', () => {
|
|
90
|
-
expect(matchesAnyAccept([], 'image/png')).toBe(false)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('should handle single accept', () => {
|
|
94
|
-
const accepts = ['image/*'] as const
|
|
95
|
-
expect(matchesAnyAccept(accepts, 'image/jpeg')).toBe(true)
|
|
96
|
-
expect(matchesAnyAccept(accepts, 'text/html')).toBe(false)
|
|
97
|
-
})
|
|
98
|
-
})
|
package/src/lib/mime.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
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: unknown): value is Accept {
|
|
24
|
-
if (typeof value !== 'string') return false
|
|
25
|
-
if (value === '*/*') return true // Fast path for the most common case
|
|
26
|
-
if (!isStringSlashString(value)) return false
|
|
27
|
-
return !value.includes('*') || value.endsWith('/*')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @note "unsafe" in that it does not check if either {@link accept} or
|
|
32
|
-
* {@link mime} are actually valid values (and could, therefore, lead to false
|
|
33
|
-
* positives if forged values are used).
|
|
34
|
-
*/
|
|
35
|
-
function matchesAcceptUnsafe(accept: Accept, mime: Mime): boolean {
|
|
36
|
-
if (accept === '*/*') {
|
|
37
|
-
return true
|
|
38
|
-
}
|
|
39
|
-
if (accept.endsWith('/*')) {
|
|
40
|
-
return mime.startsWith(accept.slice(0, -1))
|
|
41
|
-
}
|
|
42
|
-
return accept === mime
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function matchesAccept(accept: Accept, mime: string): boolean {
|
|
46
|
-
return isMime(mime) && matchesAcceptUnsafe(accept, mime)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @note "unsafe" in that it does not check if either {@link accept} or
|
|
51
|
-
* {@link mime} are actually valid values (and could, therefore, lead to false
|
|
52
|
-
* positives if forged values are used).
|
|
53
|
-
*/
|
|
54
|
-
function matchesAnyAcceptUnsafe(
|
|
55
|
-
acceptable: Iterable<Accept>,
|
|
56
|
-
mime: Mime,
|
|
57
|
-
): boolean {
|
|
58
|
-
for (const accept of acceptable) {
|
|
59
|
-
if (matchesAcceptUnsafe(accept, mime)) {
|
|
60
|
-
return true
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return false
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function matchesAnyAccept(
|
|
67
|
-
acceptable: Iterable<Accept>,
|
|
68
|
-
mime: string,
|
|
69
|
-
): boolean {
|
|
70
|
-
return isMime(mime) && matchesAnyAcceptUnsafe(acceptable, mime)
|
|
71
|
-
}
|
package/src/lib/nsid.ts
DELETED
package/src/lib/parser.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { ScopeStringSyntax } from './syntax-string.js'
|
|
2
|
-
import { NeRoArray, ParamValue, ScopeSyntax } from './syntax.js'
|
|
3
|
-
|
|
4
|
-
type InferParamPredicate<T extends (value: ParamValue) => boolean> =
|
|
5
|
-
T extends ((value: ParamValue) => value is infer U extends ParamValue)
|
|
6
|
-
? U
|
|
7
|
-
: ParamValue
|
|
8
|
-
|
|
9
|
-
type ParamsSchema = Record<
|
|
10
|
-
string,
|
|
11
|
-
| {
|
|
12
|
-
multiple: false
|
|
13
|
-
required: boolean
|
|
14
|
-
default?: ParamValue
|
|
15
|
-
normalize?: (value: ParamValue) => ParamValue
|
|
16
|
-
validate: (value: ParamValue) => boolean
|
|
17
|
-
}
|
|
18
|
-
| {
|
|
19
|
-
multiple: true
|
|
20
|
-
required: boolean
|
|
21
|
-
default?: NeRoArray<ParamValue>
|
|
22
|
-
normalize?: (value: NeRoArray<ParamValue>) => NeRoArray<ParamValue>
|
|
23
|
-
validate: (value: ParamValue) => boolean
|
|
24
|
-
}
|
|
25
|
-
>
|
|
26
|
-
|
|
27
|
-
type InferParams<S extends ParamsSchema> = {
|
|
28
|
-
[K in keyof S]:
|
|
29
|
-
| (S[K]['required'] extends true
|
|
30
|
-
? never
|
|
31
|
-
: 'default' extends keyof S[K]
|
|
32
|
-
? S[K]['default']
|
|
33
|
-
: undefined)
|
|
34
|
-
| (S[K]['multiple'] extends true
|
|
35
|
-
? NeRoArray<InferParamPredicate<S[K]['validate']>>
|
|
36
|
-
: InferParamPredicate<S[K]['validate']>)
|
|
37
|
-
} & NonNullable<unknown>
|
|
38
|
-
|
|
39
|
-
export class Parser<P extends string, S extends ParamsSchema> {
|
|
40
|
-
public readonly schemaKeys: ReadonlySet<keyof S & string>
|
|
41
|
-
|
|
42
|
-
constructor(
|
|
43
|
-
public readonly prefix: P,
|
|
44
|
-
public readonly schema: S,
|
|
45
|
-
public readonly positionalName?: keyof S & string,
|
|
46
|
-
) {
|
|
47
|
-
this.schemaKeys = new Set(Object.keys(schema))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
format(values: InferParams<S>) {
|
|
51
|
-
const params = new URLSearchParams()
|
|
52
|
-
let positional: string | undefined = undefined
|
|
53
|
-
|
|
54
|
-
for (const key of this.schemaKeys) {
|
|
55
|
-
const value = values[key]
|
|
56
|
-
// Ignore undefined values
|
|
57
|
-
if (value === undefined) continue
|
|
58
|
-
|
|
59
|
-
const schema = this.schema[key]
|
|
60
|
-
|
|
61
|
-
// Normalize the value if a normalization function is provided
|
|
62
|
-
const normalized = schema.normalize
|
|
63
|
-
? schema.normalize(value as any)
|
|
64
|
-
: value
|
|
65
|
-
|
|
66
|
-
// Ignore values that are equal to the default value
|
|
67
|
-
if (!schema.required) {
|
|
68
|
-
if (schema.default === normalized) continue
|
|
69
|
-
if (
|
|
70
|
-
schema.multiple &&
|
|
71
|
-
schema.default &&
|
|
72
|
-
arrayParamEquals(schema.default, normalized as NeRoArray<string>)
|
|
73
|
-
) {
|
|
74
|
-
continue
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (Array.isArray(normalized)) {
|
|
79
|
-
if (key === this.positionalName && normalized.length === 1) {
|
|
80
|
-
positional = String(normalized[0]!)
|
|
81
|
-
} else {
|
|
82
|
-
// remove duplicates
|
|
83
|
-
const unique = new Set(normalized.map(String))
|
|
84
|
-
for (const v of unique) params.append(key, v)
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
87
|
-
if (key === this.positionalName) {
|
|
88
|
-
positional = String(normalized)
|
|
89
|
-
} else {
|
|
90
|
-
params.set(key, String(normalized))
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return new ScopeStringSyntax(this.prefix, positional, params).toString()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// @NOTE If we needed to ever have more detailed reason as to why parsing
|
|
99
|
-
// fails, this function could easily be updated to return a
|
|
100
|
-
// ValidationResult<T> type that explains the reason for failure.
|
|
101
|
-
parse(syntax: ScopeSyntax<P>) {
|
|
102
|
-
// @NOTE no need to check prefix, since the typing (P generic) already
|
|
103
|
-
// ensures it matches
|
|
104
|
-
|
|
105
|
-
for (const key of syntax.keys()) {
|
|
106
|
-
if (!this.schemaKeys.has(key)) return null
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const result: Record<
|
|
110
|
-
string,
|
|
111
|
-
undefined | ParamValue | NeRoArray<ParamValue>
|
|
112
|
-
> = Object.create(null)
|
|
113
|
-
|
|
114
|
-
for (const key of this.schemaKeys) {
|
|
115
|
-
const definition = this.schema[key]
|
|
116
|
-
|
|
117
|
-
const param = definition.multiple
|
|
118
|
-
? syntax.getMulti(key)
|
|
119
|
-
: syntax.getSingle(key)
|
|
120
|
-
|
|
121
|
-
if (param === null) {
|
|
122
|
-
return null // Value is not valid
|
|
123
|
-
} else if (param !== undefined) {
|
|
124
|
-
if (key === this.positionalName && syntax.positional !== undefined) {
|
|
125
|
-
// Positional parameter cannot be used with named parameters
|
|
126
|
-
return null
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (definition.multiple) {
|
|
130
|
-
// Empty array is not valid
|
|
131
|
-
if (!(param as ParamValue[]).length) return null
|
|
132
|
-
if (!(param as ParamValue[]).every(definition.validate)) {
|
|
133
|
-
return null
|
|
134
|
-
}
|
|
135
|
-
} else {
|
|
136
|
-
if (!definition.validate(param as ParamValue)) {
|
|
137
|
-
return null
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
result[key] = param as ParamValue | NeRoArray<ParamValue>
|
|
142
|
-
} else if (
|
|
143
|
-
key === this.positionalName &&
|
|
144
|
-
syntax.positional !== undefined
|
|
145
|
-
) {
|
|
146
|
-
// No named parameters found, but there is a positional parameter
|
|
147
|
-
const { positional } = syntax
|
|
148
|
-
if (!definition.validate(positional)) {
|
|
149
|
-
return null
|
|
150
|
-
}
|
|
151
|
-
result[key] = definition.multiple ? [positional] : positional
|
|
152
|
-
} else if (definition.required) {
|
|
153
|
-
return null
|
|
154
|
-
} else {
|
|
155
|
-
result[key] = definition.default
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return result as InferParams<S>
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Two param arrays are considered equal if they contain the same values,
|
|
165
|
-
* regardless of the order and duplicates.
|
|
166
|
-
* @param a - The first array to compare.
|
|
167
|
-
* @param b - The second array to compare.
|
|
168
|
-
*/
|
|
169
|
-
function arrayParamEquals(
|
|
170
|
-
a: readonly unknown[],
|
|
171
|
-
b: readonly unknown[],
|
|
172
|
-
): boolean {
|
|
173
|
-
for (const item of a) if (!b.includes(item)) return false
|
|
174
|
-
for (const item of b) if (!a.includes(item)) return false
|
|
175
|
-
return true
|
|
176
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { ScopeStringFor } from './syntax.js'
|
|
2
|
-
import { Matchable } from './util.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Interface destined to provide consistency across parsed permission scopes for
|
|
6
|
-
* resources (blob, repo, etc.).
|
|
7
|
-
*/
|
|
8
|
-
export interface ResourcePermission<R extends string, T> extends Matchable<T> {
|
|
9
|
-
toString(): ScopeStringFor<R>
|
|
10
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { LexiconPermission } from './lexicon.js'
|
|
2
|
-
import { ScopeSyntax } from './syntax.js'
|
|
3
|
-
|
|
4
|
-
const isArray: (value: unknown) => value is readonly unknown[] = Array.isArray
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Translates a {@link LexiconPermission} into a {@link ScopeSyntax}.
|
|
8
|
-
*/
|
|
9
|
-
export class LexPermissionSyntax<P extends string = string>
|
|
10
|
-
implements ScopeSyntax<P>
|
|
11
|
-
{
|
|
12
|
-
constructor(readonly lexPermission: LexiconPermission<P>) {}
|
|
13
|
-
|
|
14
|
-
get prefix() {
|
|
15
|
-
return this.lexPermission.resource
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
get positional() {
|
|
19
|
-
return undefined
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
get(key: string) {
|
|
23
|
-
// Ignore reserved keywords
|
|
24
|
-
if (key === 'type') return undefined
|
|
25
|
-
if (key === 'resource') return undefined
|
|
26
|
-
|
|
27
|
-
// Ignore inherited properties (toString(), etc.)
|
|
28
|
-
if (!Object.hasOwn(this.lexPermission, key)) return undefined
|
|
29
|
-
|
|
30
|
-
return this.lexPermission[key]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
*keys() {
|
|
34
|
-
for (const key of Object.keys(this.lexPermission)) {
|
|
35
|
-
if (this.get(key) !== undefined) yield key
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getSingle(key: string) {
|
|
40
|
-
const value = this.get(key)
|
|
41
|
-
if (isArray(value)) return null
|
|
42
|
-
return value
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
getMulti(key: string) {
|
|
46
|
-
const value = this.get(key)
|
|
47
|
-
if (value === undefined) return undefined
|
|
48
|
-
if (!isArray(value)) return null
|
|
49
|
-
return value
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
toJSON() {
|
|
53
|
-
return this.lexPermission
|
|
54
|
-
}
|
|
55
|
-
}
|