@atproto/oauth-types 0.4.1 → 0.4.2

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 (97) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/atproto-loopback-client-id.d.ts +14 -0
  3. package/dist/atproto-loopback-client-id.d.ts.map +1 -0
  4. package/dist/atproto-loopback-client-id.js +43 -0
  5. package/dist/atproto-loopback-client-id.js.map +1 -0
  6. package/dist/atproto-loopback-client-metadata.d.ts +8 -1
  7. package/dist/atproto-loopback-client-metadata.d.ts.map +1 -1
  8. package/dist/atproto-loopback-client-metadata.js +13 -4
  9. package/dist/atproto-loopback-client-metadata.js.map +1 -1
  10. package/dist/atproto-loopback-client-redirect-uris.d.ts +2 -0
  11. package/dist/atproto-loopback-client-redirect-uris.d.ts.map +1 -0
  12. package/dist/atproto-loopback-client-redirect-uris.js +8 -0
  13. package/dist/atproto-loopback-client-redirect-uris.js.map +1 -0
  14. package/dist/atproto-oauth-scope.d.ts +12 -0
  15. package/dist/atproto-oauth-scope.d.ts.map +1 -0
  16. package/dist/atproto-oauth-scope.js +27 -0
  17. package/dist/atproto-oauth-scope.js.map +1 -0
  18. package/dist/atproto-oauth-token-response.d.ts +106 -0
  19. package/dist/atproto-oauth-token-response.d.ts.map +1 -0
  20. package/dist/atproto-oauth-token-response.js +15 -0
  21. package/dist/atproto-oauth-token-response.js.map +1 -0
  22. package/dist/constants.js.map +1 -1
  23. package/dist/index.d.ts +5 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +5 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/oauth-access-token.js.map +1 -1
  28. package/dist/oauth-authorization-code-grant-token-request.js.map +1 -1
  29. package/dist/oauth-authorization-details.js.map +1 -1
  30. package/dist/oauth-authorization-request-jar.js.map +1 -1
  31. package/dist/oauth-authorization-request-par.d.ts +12 -12
  32. package/dist/oauth-authorization-request-par.js.map +1 -1
  33. package/dist/oauth-authorization-request-parameters.d.ts +12 -12
  34. package/dist/oauth-authorization-request-parameters.js.map +1 -1
  35. package/dist/oauth-authorization-request-query.d.ts +12 -12
  36. package/dist/oauth-authorization-request-query.js.map +1 -1
  37. package/dist/oauth-authorization-request-uri.js.map +1 -1
  38. package/dist/oauth-authorization-response-error.js.map +1 -1
  39. package/dist/oauth-authorization-server-metadata.js.map +1 -1
  40. package/dist/oauth-client-credentials-grant-token-request.js.map +1 -1
  41. package/dist/oauth-client-credentials.js.map +1 -1
  42. package/dist/oauth-client-id-discoverable.js.map +1 -1
  43. package/dist/oauth-client-id-loopback.d.ts +24 -8
  44. package/dist/oauth-client-id-loopback.d.ts.map +1 -1
  45. package/dist/oauth-client-id-loopback.js +97 -60
  46. package/dist/oauth-client-id-loopback.js.map +1 -1
  47. package/dist/oauth-client-id.js.map +1 -1
  48. package/dist/oauth-client-metadata.d.ts +160 -1098
  49. package/dist/oauth-client-metadata.d.ts.map +1 -1
  50. package/dist/oauth-client-metadata.js.map +1 -1
  51. package/dist/oauth-code-challenge-method.js.map +1 -1
  52. package/dist/oauth-endpoint-auth-method.js.map +1 -1
  53. package/dist/oauth-endpoint-name.js.map +1 -1
  54. package/dist/oauth-grant-type.js.map +1 -1
  55. package/dist/oauth-introspection-response.js.map +1 -1
  56. package/dist/oauth-issuer-identifier.js.map +1 -1
  57. package/dist/oauth-par-response.d.ts +2 -2
  58. package/dist/oauth-par-response.js.map +1 -1
  59. package/dist/oauth-password-grant-token-request.js.map +1 -1
  60. package/dist/oauth-protected-resource-metadata.js.map +1 -1
  61. package/dist/oauth-redirect-uri.js.map +1 -1
  62. package/dist/oauth-refresh-token-grant-token-request.js.map +1 -1
  63. package/dist/oauth-refresh-token.js.map +1 -1
  64. package/dist/oauth-request-uri.js.map +1 -1
  65. package/dist/oauth-response-mode.js.map +1 -1
  66. package/dist/oauth-response-type.js.map +1 -1
  67. package/dist/oauth-scope.d.ts +5 -3
  68. package/dist/oauth-scope.d.ts.map +1 -1
  69. package/dist/oauth-scope.js +11 -8
  70. package/dist/oauth-scope.js.map +1 -1
  71. package/dist/oauth-token-identification.js.map +1 -1
  72. package/dist/oauth-token-request.js.map +1 -1
  73. package/dist/oauth-token-response.js.map +1 -1
  74. package/dist/oauth-token-type.js.map +1 -1
  75. package/dist/oidc-authorization-error-response.js.map +1 -1
  76. package/dist/oidc-claims-parameter.js.map +1 -1
  77. package/dist/oidc-claims-properties.js.map +1 -1
  78. package/dist/oidc-entity-type.js.map +1 -1
  79. package/dist/oidc-userinfo.js.map +1 -1
  80. package/dist/uri.js +1 -1
  81. package/dist/uri.js.map +1 -1
  82. package/dist/util.d.ts +9 -0
  83. package/dist/util.d.ts.map +1 -1
  84. package/dist/util.js +50 -1
  85. package/dist/util.js.map +1 -1
  86. package/package.json +3 -2
  87. package/src/atproto-loopback-client-id.ts +75 -0
  88. package/src/atproto-loopback-client-metadata.ts +33 -13
  89. package/src/atproto-loopback-client-redirect-uris.ts +4 -0
  90. package/src/atproto-oauth-scope.ts +34 -0
  91. package/src/atproto-oauth-token-response.ts +16 -0
  92. package/src/index.ts +5 -1
  93. package/src/oauth-client-id-loopback.ts +130 -72
  94. package/src/oauth-scope.ts +13 -7
  95. package/src/uri.ts +1 -1
  96. package/src/util.ts +60 -0
  97. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,23 +1,43 @@
1
1
  import {
2
- OAuthClientIdLoopback,
3
- parseOAuthLoopbackClientId,
4
- } from './oauth-client-id-loopback.js'
2
+ AtprotoLoopbackClientIdParams,
3
+ OAuthLoopbackClientIdConfig,
4
+ buildAtprotoLoopbackClientId,
5
+ parseAtprotoLoopbackClientId,
6
+ } from './atproto-loopback-client-id.js'
7
+ import { AtprotoOAuthScope } from './atproto-oauth-scope.js'
8
+ import { OAuthClientIdLoopback } from './oauth-client-id-loopback.js'
5
9
  import { OAuthClientMetadataInput } from './oauth-client-metadata.js'
10
+ import { OAuthLoopbackRedirectURI } from './oauth-redirect-uri.js'
11
+
12
+ export type AtprotoLoopbackClientMetadata = OAuthClientMetadataInput & {
13
+ client_id: OAuthClientIdLoopback
14
+ scope: AtprotoOAuthScope
15
+ redirect_uris: [OAuthLoopbackRedirectURI, ...OAuthLoopbackRedirectURI[]]
16
+ }
6
17
 
7
18
  export function atprotoLoopbackClientMetadata(
8
19
  clientId: string,
9
- ): OAuthClientMetadataInput & {
10
- client_id: OAuthClientIdLoopback
11
- } {
12
- const {
13
- scope = 'atproto',
14
- redirect_uris = [`http://127.0.0.1/`, `http://[::1]/`],
15
- } = parseOAuthLoopbackClientId(clientId)
20
+ ): AtprotoLoopbackClientMetadata {
21
+ const params = parseAtprotoLoopbackClientId(clientId)
22
+ // Safe to cast because parseAtprotoLoopbackClientId ensures it's a loopback ID
23
+ return buildMetadataInternal(clientId as OAuthClientIdLoopback, params)
24
+ }
25
+
26
+ export function buildAtprotoLoopbackClientMetadata(
27
+ config: OAuthLoopbackClientIdConfig,
28
+ ): AtprotoLoopbackClientMetadata {
29
+ const clientId = buildAtprotoLoopbackClientId(config)
30
+ return buildMetadataInternal(clientId, parseAtprotoLoopbackClientId(clientId))
31
+ }
16
32
 
33
+ function buildMetadataInternal(
34
+ clientId: OAuthClientIdLoopback,
35
+ clientParams: AtprotoLoopbackClientIdParams,
36
+ ): AtprotoLoopbackClientMetadata {
17
37
  return {
18
- client_id: clientId as OAuthClientIdLoopback,
19
- scope,
20
- redirect_uris,
38
+ client_id: clientId,
39
+ scope: clientParams.scope,
40
+ redirect_uris: clientParams.redirect_uris,
21
41
  response_types: ['code'],
22
42
  grant_types: ['authorization_code', 'refresh_token'],
23
43
  token_endpoint_auth_method: 'none',
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_LOOPBACK_CLIENT_REDIRECT_URIS = Object.freeze([
2
+ `http://127.0.0.1/`,
3
+ `http://[::1]/`,
4
+ ] as const)
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod'
2
+ import { OAuthScope, isOAuthScope } from './oauth-scope.js'
3
+ import { SpaceSeparatedValue, isSpaceSeparatedValue } from './util.js'
4
+
5
+ export const ATPROTO_SCOPE_VALUE = 'atproto'
6
+ export type AtprotoScopeValue = typeof ATPROTO_SCOPE_VALUE
7
+
8
+ export type AtprotoOAuthScope = OAuthScope &
9
+ SpaceSeparatedValue<AtprotoScopeValue>
10
+
11
+ export function isAtprotoOAuthScope(input: string): input is AtprotoOAuthScope {
12
+ return (
13
+ isOAuthScope(input) && isSpaceSeparatedValue(ATPROTO_SCOPE_VALUE, input)
14
+ )
15
+ }
16
+
17
+ export function asAtprotoOAuthScope<I extends string>(input: I) {
18
+ if (isAtprotoOAuthScope(input)) return input
19
+ throw new TypeError(`Value must contain "${ATPROTO_SCOPE_VALUE}" scope value`)
20
+ }
21
+
22
+ export function assertAtprotoOAuthScope(
23
+ input: string,
24
+ ): asserts input is AtprotoOAuthScope {
25
+ void asAtprotoOAuthScope(input)
26
+ }
27
+
28
+ export const atprotoOAuthScopeSchema = z.string().refine(isAtprotoOAuthScope, {
29
+ message: 'Invalid ATProto OAuth scope',
30
+ })
31
+
32
+ // Default scope is for reading identity (did) only
33
+ export const DEFAULT_ATPROTO_OAUTH_SCOPE =
34
+ ATPROTO_SCOPE_VALUE satisfies AtprotoOAuthScope
@@ -0,0 +1,16 @@
1
+ import { TypeOf, z } from 'zod'
2
+ import { atprotoDidSchema } from '@atproto/did'
3
+ import { atprotoOAuthScopeSchema } from './atproto-oauth-scope'
4
+ import { oauthTokenResponseSchema } from './oauth-token-response.js'
5
+
6
+ export const atprotoOAuthTokenResponseSchema = oauthTokenResponseSchema.extend({
7
+ token_type: z.literal('DPoP'),
8
+ sub: atprotoDidSchema,
9
+ scope: atprotoOAuthScopeSchema,
10
+ // OpenID is not compatible with atproto identities
11
+ id_token: z.never().optional(),
12
+ })
13
+
14
+ export type AtprotoOAuthTokenResponse = TypeOf<
15
+ typeof atprotoOAuthTokenResponseSchema
16
+ >
package/src/index.ts CHANGED
@@ -2,9 +2,12 @@ export * from './constants.js'
2
2
  export * from './uri.js'
3
3
  export * from './util.js'
4
4
 
5
+ export * from './atproto-loopback-client-id.js'
5
6
  export * from './atproto-loopback-client-metadata.js'
7
+ export * from './atproto-loopback-client-redirect-uris.js'
8
+ export * from './atproto-oauth-scope.js'
9
+ export * from './atproto-oauth-token-response.js'
6
10
  export * from './oauth-access-token.js'
7
- export * from './oauth-authorization-response-error.js'
8
11
  export * from './oauth-authorization-code-grant-token-request.js'
9
12
  export * from './oauth-authorization-details.js'
10
13
  export * from './oauth-authorization-request-jar.js'
@@ -12,6 +15,7 @@ export * from './oauth-authorization-request-par.js'
12
15
  export * from './oauth-authorization-request-parameters.js'
13
16
  export * from './oauth-authorization-request-query.js'
14
17
  export * from './oauth-authorization-request-uri.js'
18
+ export * from './oauth-authorization-response-error.js'
15
19
  export * from './oauth-authorization-server-metadata.js'
16
20
  export * from './oauth-client-credentials-grant-token-request.js'
17
21
  export * from './oauth-client-credentials.js'
@@ -1,106 +1,164 @@
1
- import { TypeOf, ZodIssueCode } from 'zod'
2
1
  import { oauthClientIdSchema } from './oauth-client-id.js'
3
2
  import {
4
3
  OAuthLoopbackRedirectURI,
5
- OAuthRedirectUri,
6
4
  oauthLoopbackRedirectURISchema,
7
5
  } from './oauth-redirect-uri.js'
8
6
  import { OAuthScope, oauthScopeSchema } from './oauth-scope.js'
9
7
 
10
- const PREFIX = 'http://localhost'
8
+ export const LOOPBACK_CLIENT_ID_ORIGIN = 'http://localhost'
9
+
10
+ // @NOTE This is not actually based on a standard, but rather a convention
11
+ // established by Bluesky in the Atproto specs and implementation. As such, and
12
+ // in order to respect the convention from this package, these should be
13
+ // prefixed with "Atproto" instead of "OAuth". For legacy reasons, we keep the
14
+ // current names, but we should rename them in a future major release, unless
15
+ // loopback client ids have since then been standardized.
16
+
17
+ export type OAuthClientIdLoopback =
18
+ `http://localhost${'' | `/`}${'' | `?${string}`}`
19
+
20
+ export type OAuthLoopbackClientIdParams = {
21
+ scope?: OAuthScope
22
+ redirect_uris?: [OAuthLoopbackRedirectURI, ...OAuthLoopbackRedirectURI[]]
23
+ }
11
24
 
12
25
  export const oauthClientIdLoopbackSchema = oauthClientIdSchema.superRefine(
13
- (value, ctx): value is `${typeof PREFIX}${'' | '/'}${'' | `?${string}`}` => {
14
- try {
15
- assertOAuthLoopbackClientId(value)
16
- return true
17
- } catch (error) {
18
- ctx.addIssue({
19
- code: ZodIssueCode.custom,
20
- message:
21
- error instanceof TypeError
22
- ? error.message
23
- : 'Invalid loopback client ID',
24
- })
25
- return false
26
+ (input, ctx): input is OAuthClientIdLoopback => {
27
+ const result = safeParseOAuthLoopbackClientId(input)
28
+ if (!result.success) {
29
+ ctx.addIssue({ code: 'custom', message: result.message })
26
30
  }
31
+ return result.success
27
32
  },
28
33
  )
29
34
 
30
- export type OAuthClientIdLoopback = TypeOf<typeof oauthClientIdLoopbackSchema>
35
+ export function assertOAuthLoopbackClientId(
36
+ input: string,
37
+ ): asserts input is OAuthClientIdLoopback {
38
+ void parseOAuthLoopbackClientId(input)
39
+ }
31
40
 
32
- export function isOAuthClientIdLoopback(
33
- clientId: string,
34
- ): clientId is OAuthClientIdLoopback {
35
- try {
36
- parseOAuthLoopbackClientId(clientId)
37
- return true
38
- } catch {
39
- return false
40
- }
41
+ export function isOAuthClientIdLoopback<T extends string>(
42
+ input: T,
43
+ ): input is T & OAuthClientIdLoopback {
44
+ return safeParseOAuthLoopbackClientId(input).success
41
45
  }
42
46
 
43
- export function assertOAuthLoopbackClientId(
44
- clientId: string,
45
- ): asserts clientId is OAuthClientIdLoopback {
46
- void parseOAuthLoopbackClientId(clientId)
47
+ export function asOAuthClientIdLoopback<T extends string>(input: T) {
48
+ assertOAuthLoopbackClientId(input)
49
+ return input
47
50
  }
48
51
 
49
- // @TODO should we turn this into a zod schema? (more coherent error with other
50
- // validation functions)
51
- export function parseOAuthLoopbackClientId(clientId: string): {
52
- scope?: OAuthScope
53
- redirect_uris?: [OAuthRedirectUri, ...OAuthRedirectUri[]]
54
- } {
55
- if (!clientId.startsWith(PREFIX)) {
56
- throw new TypeError(`Loopback ClientID must start with "${PREFIX}"`)
57
- } else if (clientId.includes('#', PREFIX.length)) {
58
- throw new TypeError('Loopback ClientID must not contain a hash component')
59
- }
52
+ export function parseOAuthLoopbackClientId(
53
+ input: string,
54
+ ): OAuthLoopbackClientIdParams {
55
+ const result = safeParseOAuthLoopbackClientId(input)
56
+ if (result.success) return result.value
60
57
 
61
- const queryStringIdx =
62
- clientId.length > PREFIX.length && clientId[PREFIX.length] === '/'
63
- ? PREFIX.length + 1
64
- : PREFIX.length
58
+ throw new TypeError(`Invalid loopback client ID: ${result.message}`)
59
+ }
65
60
 
66
- if (clientId.length === queryStringIdx) {
67
- return {} // no query string to parse
61
+ /**
62
+ * Similar to Zod's {@link SafeParseReturnType} but uses a simple "message"
63
+ * string instead of an "error" Error object.
64
+ */
65
+ type LightParseReturnType<T> =
66
+ | { success: true; value: T }
67
+ | { success: false; message: string }
68
+
69
+ export function safeParseOAuthLoopbackClientId(
70
+ input: string,
71
+ ): LightParseReturnType<OAuthLoopbackClientIdParams> {
72
+ // @NOTE Not using "new URL" to ensure input indeed matches the type
73
+ // OAuthClientIdLoopback
74
+
75
+ if (!input.startsWith(LOOPBACK_CLIENT_ID_ORIGIN)) {
76
+ return {
77
+ success: false,
78
+ message: `Value must start with "${LOOPBACK_CLIENT_ID_ORIGIN}"`,
79
+ }
68
80
  }
69
81
 
70
- if (clientId[queryStringIdx] !== '?') {
71
- throw new TypeError('Loopback ClientID must not contain a path component')
82
+ if (input.includes('#', LOOPBACK_CLIENT_ID_ORIGIN.length)) {
83
+ return {
84
+ success: false,
85
+ message: 'Value must not contain a hash component',
86
+ }
72
87
  }
73
88
 
74
- const searchParams = new URLSearchParams(clientId.slice(queryStringIdx + 1))
89
+ // Since we don't allow a path component (except for a single "/") the query
90
+ // string starts after the origin (+ 1 if there is a "/")
91
+ const queryStringIdx =
92
+ input.length > LOOPBACK_CLIENT_ID_ORIGIN.length &&
93
+ input.charCodeAt(LOOPBACK_CLIENT_ID_ORIGIN.length) === 0x2f /* '/' */
94
+ ? LOOPBACK_CLIENT_ID_ORIGIN.length + 1
95
+ : LOOPBACK_CLIENT_ID_ORIGIN.length
75
96
 
76
- for (const name of searchParams.keys()) {
77
- if (name !== 'redirect_uri' && name !== 'scope') {
78
- throw new TypeError(`Invalid query parameter "${name}" in client ID`)
97
+ // Since we determined the position of the query string based on the origin
98
+ // length (instead of looking for a "?"), we need to make sure the query
99
+ // string position (if any) indeed starts with a "?".
100
+ if (
101
+ input.length !== queryStringIdx &&
102
+ input.charCodeAt(queryStringIdx) !== 0x3f /* '?' */
103
+ ) {
104
+ return {
105
+ success: false,
106
+ message: 'Value must not contain a path component',
79
107
  }
80
108
  }
81
109
 
82
- const scope = searchParams.get('scope') ?? undefined
83
- if (scope != null) {
84
- if (searchParams.getAll('scope').length > 1) {
85
- throw new TypeError(
86
- 'Loopback ClientID must contain at most one scope query parameter',
87
- )
88
- } else if (!oauthScopeSchema.safeParse(scope).success) {
89
- throw new TypeError('Invalid scope query parameter in client ID')
110
+ const queryString = input.slice(queryStringIdx + 1)
111
+ return safeParseOAuthLoopbackClientIdQueryString(queryString)
112
+ }
113
+
114
+ export function safeParseOAuthLoopbackClientIdQueryString(
115
+ input: string | Iterable<[key: string, value: string]>,
116
+ ): LightParseReturnType<OAuthLoopbackClientIdParams> {
117
+ // Parse query params
118
+ const params: OAuthLoopbackClientIdParams = {}
119
+
120
+ const it = typeof input === 'string' ? new URLSearchParams(input) : input
121
+ for (const [key, value] of it) {
122
+ if (key === 'scope') {
123
+ if ('scope' in params) {
124
+ return {
125
+ success: false,
126
+ message: 'Duplicate "scope" query parameter',
127
+ }
128
+ }
129
+
130
+ const res = oauthScopeSchema.safeParse(value)
131
+ if (!res.success) {
132
+ const reason = res.error.issues.map((i) => i.message).join(', ')
133
+ return {
134
+ success: false,
135
+ message: `Invalid "scope" query parameter: ${reason || 'Validation failed'}`,
136
+ }
137
+ }
138
+
139
+ params.scope = res.data
140
+ } else if (key === 'redirect_uri') {
141
+ const res = oauthLoopbackRedirectURISchema.safeParse(value)
142
+ if (!res.success) {
143
+ const reason = res.error.issues.map((i) => i.message).join(', ')
144
+ return {
145
+ success: false,
146
+ message: `Invalid "redirect_uri" query parameter: ${reason || 'Validation failed'}`,
147
+ }
148
+ }
149
+
150
+ if (params.redirect_uris == null) params.redirect_uris = [res.data]
151
+ else params.redirect_uris.push(res.data)
152
+ } else {
153
+ return {
154
+ success: false,
155
+ message: `Unexpected query parameter "${key}"`,
156
+ }
90
157
  }
91
158
  }
92
159
 
93
- const redirect_uris = searchParams.has('redirect_uri')
94
- ? (searchParams
95
- .getAll('redirect_uri')
96
- .map((value) => oauthLoopbackRedirectURISchema.parse(value)) as [
97
- OAuthLoopbackRedirectURI,
98
- ...OAuthLoopbackRedirectURI[],
99
- ])
100
- : undefined
101
-
102
160
  return {
103
- scope,
104
- redirect_uris,
161
+ success: true,
162
+ value: params,
105
163
  }
106
164
  }
@@ -1,15 +1,21 @@
1
1
  import { z } from 'zod'
2
2
 
3
+ // scope = scope-token *( SP scope-token )
4
+ // scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
5
+ export const OAUTH_SCOPE_REGEXP =
6
+ /^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/
7
+
8
+ export const isOAuthScope = (input: string): boolean =>
9
+ OAUTH_SCOPE_REGEXP.test(input)
10
+
3
11
  /**
4
- * A space separated list of most non-control ASCII characters except backslash
5
- * and double quote.
12
+ * A (single) space separated list of non empty printable ASCII char string
13
+ * (except backslash and double quote).
6
14
  *
7
15
  * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-1.4.1}
8
16
  */
9
- export const oauthScopeSchema = z
10
- .string()
11
- // scope = scope-token *( SP scope-token )
12
- // scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
13
- .regex(/^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/)
17
+ export const oauthScopeSchema = z.string().refine(isOAuthScope, {
18
+ message: 'Invalid OAuth scope',
19
+ })
14
20
 
15
21
  export type OAuthScope = z.infer<typeof oauthScopeSchema>
package/src/uri.ts CHANGED
@@ -3,7 +3,7 @@ import { isHostnameIP, isLoopbackHost } from './util.js'
3
3
 
4
4
  const canParseUrl =
5
5
  // eslint-disable-next-line n/no-unsupported-features/node-builtins
6
- URL.canParse ??
6
+ URL.canParse?.bind(URL) ??
7
7
  // URL.canParse is not available in Node.js < 18.7.0
8
8
  ((urlStr: string): boolean => {
9
9
  try {
package/src/util.ts CHANGED
@@ -86,3 +86,63 @@ export const numberPreprocess = (val: unknown): unknown => {
86
86
  }
87
87
  return val
88
88
  }
89
+
90
+ /**
91
+ * Returns true if the two arrays contain the same elements, regardless of order
92
+ * or duplicates.
93
+ */
94
+ export function arrayEquivalent<T>(a: readonly T[], b: readonly T[]) {
95
+ if (a === b) return true
96
+ return a.every(includedIn, b) && b.every(includedIn, a)
97
+ }
98
+
99
+ export function includedIn<T>(this: readonly T[], item: T) {
100
+ return this.includes(item)
101
+ }
102
+
103
+ export function asArray<T>(
104
+ value: Iterable<T> | undefined,
105
+ ): undefined | readonly T[] {
106
+ if (value == null) return undefined
107
+ if (Array.isArray(value)) return value // already a (possibly readonly) array
108
+ return Array.from(value)
109
+ }
110
+
111
+ export type SpaceSeparatedValue<Value extends string> =
112
+ `${'' | `${string} `}${Value}${'' | ` ${string}`}`
113
+
114
+ export const isSpaceSeparatedValue = <Value extends string>(
115
+ value: Value,
116
+ input: string,
117
+ ): input is SpaceSeparatedValue<Value> => {
118
+ if (value.length === 0) throw new TypeError('Value cannot be empty')
119
+ if (value.includes(' ')) throw new TypeError('Value cannot contain spaces')
120
+
121
+ // Optimized version of:
122
+ // return input.split(' ').includes(value)
123
+
124
+ const inputLength = input.length
125
+ const valueLength = value.length
126
+
127
+ if (inputLength < valueLength) return false
128
+
129
+ let idx = input.indexOf(value)
130
+ let idxEnd: number
131
+
132
+ while (idx !== -1) {
133
+ idxEnd = idx + valueLength
134
+
135
+ if (
136
+ // at beginning or preceded by space
137
+ (idx === 0 || input.charCodeAt(idx - 1) === 32) &&
138
+ // at end or followed by space
139
+ (idxEnd === inputLength || input.charCodeAt(idxEnd) === 32)
140
+ ) {
141
+ return true
142
+ }
143
+
144
+ idx = input.indexOf(value, idxEnd + 1)
145
+ }
146
+
147
+ return false
148
+ }
@@ -1 +1 @@
1
- {"root":["./src/atproto-loopback-client-metadata.ts","./src/constants.ts","./src/index.ts","./src/oauth-access-token.ts","./src/oauth-authorization-code-grant-token-request.ts","./src/oauth-authorization-details.ts","./src/oauth-authorization-request-jar.ts","./src/oauth-authorization-request-par.ts","./src/oauth-authorization-request-parameters.ts","./src/oauth-authorization-request-query.ts","./src/oauth-authorization-request-uri.ts","./src/oauth-authorization-response-error.ts","./src/oauth-authorization-server-metadata.ts","./src/oauth-client-credentials-grant-token-request.ts","./src/oauth-client-credentials.ts","./src/oauth-client-id-discoverable.ts","./src/oauth-client-id-loopback.ts","./src/oauth-client-id.ts","./src/oauth-client-metadata.ts","./src/oauth-code-challenge-method.ts","./src/oauth-endpoint-auth-method.ts","./src/oauth-endpoint-name.ts","./src/oauth-grant-type.ts","./src/oauth-introspection-response.ts","./src/oauth-issuer-identifier.ts","./src/oauth-par-response.ts","./src/oauth-password-grant-token-request.ts","./src/oauth-protected-resource-metadata.ts","./src/oauth-redirect-uri.ts","./src/oauth-refresh-token-grant-token-request.ts","./src/oauth-refresh-token.ts","./src/oauth-request-uri.ts","./src/oauth-response-mode.ts","./src/oauth-response-type.ts","./src/oauth-scope.ts","./src/oauth-token-identification.ts","./src/oauth-token-request.ts","./src/oauth-token-response.ts","./src/oauth-token-type.ts","./src/oidc-authorization-error-response.ts","./src/oidc-claims-parameter.ts","./src/oidc-claims-properties.ts","./src/oidc-entity-type.ts","./src/oidc-userinfo.ts","./src/uri.ts","./src/util.ts"],"version":"5.8.2"}
1
+ {"root":["./src/atproto-loopback-client-id.ts","./src/atproto-loopback-client-metadata.ts","./src/atproto-loopback-client-redirect-uris.ts","./src/atproto-oauth-scope.ts","./src/atproto-oauth-token-response.ts","./src/constants.ts","./src/index.ts","./src/oauth-access-token.ts","./src/oauth-authorization-code-grant-token-request.ts","./src/oauth-authorization-details.ts","./src/oauth-authorization-request-jar.ts","./src/oauth-authorization-request-par.ts","./src/oauth-authorization-request-parameters.ts","./src/oauth-authorization-request-query.ts","./src/oauth-authorization-request-uri.ts","./src/oauth-authorization-response-error.ts","./src/oauth-authorization-server-metadata.ts","./src/oauth-client-credentials-grant-token-request.ts","./src/oauth-client-credentials.ts","./src/oauth-client-id-discoverable.ts","./src/oauth-client-id-loopback.ts","./src/oauth-client-id.ts","./src/oauth-client-metadata.ts","./src/oauth-code-challenge-method.ts","./src/oauth-endpoint-auth-method.ts","./src/oauth-endpoint-name.ts","./src/oauth-grant-type.ts","./src/oauth-introspection-response.ts","./src/oauth-issuer-identifier.ts","./src/oauth-par-response.ts","./src/oauth-password-grant-token-request.ts","./src/oauth-protected-resource-metadata.ts","./src/oauth-redirect-uri.ts","./src/oauth-refresh-token-grant-token-request.ts","./src/oauth-refresh-token.ts","./src/oauth-request-uri.ts","./src/oauth-response-mode.ts","./src/oauth-response-type.ts","./src/oauth-scope.ts","./src/oauth-token-identification.ts","./src/oauth-token-request.ts","./src/oauth-token-response.ts","./src/oauth-token-type.ts","./src/oidc-authorization-error-response.ts","./src/oidc-claims-parameter.ts","./src/oidc-claims-properties.ts","./src/oidc-entity-type.ts","./src/oidc-userinfo.ts","./src/uri.ts","./src/util.ts"],"version":"5.8.2"}