@atproto/oauth-types 0.1.5 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/atproto-loopback-client-metadata.d.ts +4 -1
  3. package/dist/atproto-loopback-client-metadata.d.ts.map +1 -1
  4. package/dist/atproto-loopback-client-metadata.js +1 -2
  5. package/dist/atproto-loopback-client-metadata.js.map +1 -1
  6. package/dist/constants.d.ts +0 -6
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +1 -17
  9. package/dist/constants.js.map +1 -1
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/oauth-authorization-code-grant-token-request.d.ts +2 -2
  15. package/dist/oauth-authorization-code-grant-token-request.d.ts.map +1 -1
  16. package/dist/oauth-authorization-code-grant-token-request.js +2 -1
  17. package/dist/oauth-authorization-code-grant-token-request.js.map +1 -1
  18. package/dist/oauth-authorization-details.d.ts +42 -4
  19. package/dist/oauth-authorization-details.d.ts.map +1 -1
  20. package/dist/oauth-authorization-details.js +21 -1
  21. package/dist/oauth-authorization-details.js.map +1 -1
  22. package/dist/oauth-authorization-request-jar.d.ts +1 -1
  23. package/dist/oauth-authorization-request-par.d.ts +11 -11
  24. package/dist/oauth-authorization-request-parameters.d.ts +10 -10
  25. package/dist/oauth-authorization-request-parameters.d.ts.map +1 -1
  26. package/dist/oauth-authorization-request-parameters.js +3 -2
  27. package/dist/oauth-authorization-request-parameters.js.map +1 -1
  28. package/dist/oauth-authorization-request-query.d.ts +11 -11
  29. package/dist/oauth-authorization-server-metadata.d.ts +69 -66
  30. package/dist/oauth-authorization-server-metadata.d.ts.map +1 -1
  31. package/dist/oauth-authorization-server-metadata.js +14 -10
  32. package/dist/oauth-authorization-server-metadata.js.map +1 -1
  33. package/dist/oauth-client-id-discoverable.d.ts +3 -2
  34. package/dist/oauth-client-id-discoverable.d.ts.map +1 -1
  35. package/dist/oauth-client-id-discoverable.js +54 -31
  36. package/dist/oauth-client-id-discoverable.js.map +1 -1
  37. package/dist/oauth-client-id-loopback.d.ts +5 -5
  38. package/dist/oauth-client-id-loopback.d.ts.map +1 -1
  39. package/dist/oauth-client-id-loopback.js +32 -31
  40. package/dist/oauth-client-id-loopback.js.map +1 -1
  41. package/dist/oauth-client-metadata.d.ts +112 -102
  42. package/dist/oauth-client-metadata.d.ts.map +1 -1
  43. package/dist/oauth-client-metadata.js +18 -8
  44. package/dist/oauth-client-metadata.js.map +1 -1
  45. package/dist/oauth-issuer-identifier.d.ts +2 -1
  46. package/dist/oauth-issuer-identifier.d.ts.map +1 -1
  47. package/dist/oauth-issuer-identifier.js +8 -23
  48. package/dist/oauth-issuer-identifier.js.map +1 -1
  49. package/dist/oauth-protected-resource-metadata.d.ts +15 -12
  50. package/dist/oauth-protected-resource-metadata.d.ts.map +1 -1
  51. package/dist/oauth-protected-resource-metadata.js +15 -5
  52. package/dist/oauth-protected-resource-metadata.js.map +1 -1
  53. package/dist/oauth-redirect-uri.d.ts +10 -0
  54. package/dist/oauth-redirect-uri.d.ts.map +1 -0
  55. package/dist/oauth-redirect-uri.js +35 -0
  56. package/dist/oauth-redirect-uri.js.map +1 -0
  57. package/dist/oauth-refresh-token-grant-token-request.d.ts +0 -3
  58. package/dist/oauth-refresh-token-grant-token-request.d.ts.map +1 -1
  59. package/dist/oauth-refresh-token-grant-token-request.js +0 -2
  60. package/dist/oauth-refresh-token-grant-token-request.js.map +1 -1
  61. package/dist/oauth-token-request.d.ts +2 -5
  62. package/dist/oauth-token-request.d.ts.map +1 -1
  63. package/dist/oauth-token-response.d.ts +9 -12
  64. package/dist/oauth-token-response.d.ts.map +1 -1
  65. package/dist/oauth-token-response.js +4 -2
  66. package/dist/oauth-token-response.js.map +1 -1
  67. package/dist/uri.d.ts +20 -0
  68. package/dist/uri.d.ts.map +1 -0
  69. package/dist/uri.js +127 -0
  70. package/dist/uri.js.map +1 -0
  71. package/dist/util.js +5 -6
  72. package/dist/util.js.map +1 -1
  73. package/package.json +2 -2
  74. package/src/atproto-loopback-client-metadata.ts +8 -3
  75. package/src/constants.ts +0 -16
  76. package/src/index.ts +2 -0
  77. package/src/oauth-authorization-code-grant-token-request.ts +2 -1
  78. package/src/oauth-authorization-details.ts +21 -1
  79. package/src/oauth-authorization-request-parameters.ts +3 -2
  80. package/src/oauth-authorization-server-metadata.ts +14 -10
  81. package/src/oauth-client-id-discoverable.ts +69 -51
  82. package/src/oauth-client-id-loopback.ts +40 -40
  83. package/src/oauth-client-metadata.ts +18 -8
  84. package/src/oauth-issuer-identifier.ts +14 -24
  85. package/src/oauth-protected-resource-metadata.ts +15 -5
  86. package/src/oauth-redirect-uri.ts +56 -0
  87. package/src/oauth-refresh-token-grant-token-request.ts +0 -2
  88. package/src/oauth-token-response.ts +4 -2
  89. package/src/uri.ts +171 -0
  90. package/tsconfig.build.tsbuildinfo +1 -0
@@ -1,11 +1,33 @@
1
- import { OAuthClientId } from './oauth-client-id.js'
1
+ import { TypeOf, ZodIssueCode } from 'zod'
2
+ import { oauthClientIdSchema } from './oauth-client-id.js'
3
+ import {
4
+ OAuthLoopbackRedirectURI,
5
+ oauthLoopbackRedirectURISchema,
6
+ OAuthRedirectUri,
7
+ } from './oauth-redirect-uri.js'
2
8
  import { OAuthScope, oauthScopeSchema } from './oauth-scope.js'
3
- import { isLoopbackHost, safeUrl } from './util.js'
4
9
 
5
- const OAUTH_CLIENT_ID_LOOPBACK_URL = 'http://localhost'
10
+ const PREFIX = 'http://localhost'
6
11
 
7
- export type OAuthClientIdLoopback = OAuthClientId &
8
- `${typeof OAUTH_CLIENT_ID_LOOPBACK_URL}${'' | '/'}${'' | `?${string}`}`
12
+ 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
+ }
27
+ },
28
+ )
29
+
30
+ export type OAuthClientIdLoopback = TypeOf<typeof oauthClientIdLoopbackSchema>
9
31
 
10
32
  export function isOAuthClientIdLoopback(
11
33
  clientId: string,
@@ -28,21 +50,18 @@ export function assertOAuthLoopbackClientId(
28
50
  // validation functions)
29
51
  export function parseOAuthLoopbackClientId(clientId: string): {
30
52
  scope?: OAuthScope
31
- redirect_uris?: [string, ...string[]]
53
+ redirect_uris?: [OAuthRedirectUri, ...OAuthRedirectUri[]]
32
54
  } {
33
- if (!clientId.startsWith(OAUTH_CLIENT_ID_LOOPBACK_URL)) {
34
- throw new TypeError(
35
- `Loopback ClientID must start with "${OAUTH_CLIENT_ID_LOOPBACK_URL}"`,
36
- )
37
- } else if (clientId.includes('#', OAUTH_CLIENT_ID_LOOPBACK_URL.length)) {
55
+ if (!clientId.startsWith(PREFIX)) {
56
+ throw new TypeError(`Loopback ClientID must start with "${PREFIX}"`)
57
+ } else if (clientId.includes('#', PREFIX.length)) {
38
58
  throw new TypeError('Loopback ClientID must not contain a hash component')
39
59
  }
40
60
 
41
61
  const queryStringIdx =
42
- clientId.length > OAUTH_CLIENT_ID_LOOPBACK_URL.length &&
43
- clientId[OAUTH_CLIENT_ID_LOOPBACK_URL.length] === '/'
44
- ? OAUTH_CLIENT_ID_LOOPBACK_URL.length + 1
45
- : OAUTH_CLIENT_ID_LOOPBACK_URL.length
62
+ clientId.length > PREFIX.length && clientId[PREFIX.length] === '/'
63
+ ? PREFIX.length + 1
64
+ : PREFIX.length
46
65
 
47
66
  if (clientId.length === queryStringIdx) {
48
67
  return {} // no query string to parse
@@ -72,33 +91,14 @@ export function parseOAuthLoopbackClientId(clientId: string): {
72
91
  }
73
92
 
74
93
  const redirect_uris = searchParams.has('redirect_uri')
75
- ? (searchParams.getAll('redirect_uri') as [string, ...string[]])
94
+ ? (searchParams
95
+ .getAll('redirect_uri')
96
+ .map((value) => oauthLoopbackRedirectURISchema.parse(value)) as [
97
+ OAuthLoopbackRedirectURI,
98
+ ...OAuthLoopbackRedirectURI[],
99
+ ])
76
100
  : undefined
77
101
 
78
- if (redirect_uris) {
79
- for (const uri of redirect_uris) {
80
- const url = safeUrl(uri)
81
- if (!url) {
82
- throw new TypeError(`Invalid redirect_uri in client ID: ${uri}`)
83
- }
84
- if (url.protocol !== 'http:') {
85
- throw new TypeError(
86
- `Loopback ClientID must use "http:" redirect_uri's (got ${uri})`,
87
- )
88
- }
89
- if (url.hostname === 'localhost') {
90
- throw new TypeError(
91
- `Loopback ClientID must not use "localhost" as redirect_uri hostname (got ${uri})`,
92
- )
93
- }
94
- if (!isLoopbackHost(url.hostname)) {
95
- throw new TypeError(
96
- `Loopback ClientID must use loopback addresses as redirect_uri's (got ${uri})`,
97
- )
98
- }
99
- }
100
- }
101
-
102
102
  return {
103
103
  scope,
104
104
  redirect_uris,
@@ -4,13 +4,23 @@ import { z } from 'zod'
4
4
  import { oauthClientIdSchema } from './oauth-client-id.js'
5
5
  import { oauthEndpointAuthMethod } from './oauth-endpoint-auth-method.js'
6
6
  import { oauthGrantTypeSchema } from './oauth-grant-type.js'
7
+ import { oauthRedirectUriSchema } from './oauth-redirect-uri.js'
7
8
  import { oauthResponseTypeSchema } from './oauth-response-type.js'
8
9
  import { oauthScopeSchema } from './oauth-scope.js'
10
+ import { webUriSchema } from './uri.js'
9
11
 
10
- // https://openid.net/specs/openid-connect-registration-1_0.html
11
- // https://datatracker.ietf.org/doc/html/rfc7591
12
+ /**
13
+ * @see {@link https://openid.net/specs/openid-connect-registration-1_0.html}
14
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7591}
15
+ * @note we do not enforce https: scheme in URIs to support development
16
+ * environments. Make sure to validate the URIs before using it in a production
17
+ * environment.
18
+ */
12
19
  export const oauthClientMetadataSchema = z.object({
13
- redirect_uris: z.array(z.string().url()).nonempty(),
20
+ /**
21
+ * @note redirect_uris require additional validation
22
+ */
23
+ redirect_uris: z.array(oauthRedirectUriSchema).nonempty(),
14
24
  response_types: z
15
25
  .array(oauthResponseTypeSchema)
16
26
  .nonempty()
@@ -30,7 +40,7 @@ export const oauthClientMetadataSchema = z.object({
30
40
  token_endpoint_auth_signing_alg: z.string().optional(),
31
41
  userinfo_signed_response_alg: z.string().optional(),
32
42
  userinfo_encrypted_response_alg: z.string().optional(),
33
- jwks_uri: z.string().url().optional(),
43
+ jwks_uri: webUriSchema.optional(),
34
44
  jwks: jwksPubSchema.optional(),
35
45
  application_type: z.enum(['web', 'native']).default('web').optional(), // default, per spec, is "web"
36
46
  subject_type: z.enum(['public', 'pairwise']).default('public').optional(),
@@ -41,10 +51,10 @@ export const oauthClientMetadataSchema = z.object({
41
51
  authorization_encrypted_response_alg: z.string().optional(),
42
52
  client_id: oauthClientIdSchema.optional(),
43
53
  client_name: z.string().optional(),
44
- client_uri: z.string().url().optional(),
45
- policy_uri: z.string().url().optional(),
46
- tos_uri: z.string().url().optional(),
47
- logo_uri: z.string().url().optional(),
54
+ client_uri: webUriSchema.optional(),
55
+ policy_uri: webUriSchema.optional(),
56
+ tos_uri: webUriSchema.optional(),
57
+ logo_uri: webUriSchema.optional(), // TODO: allow data: uri ?
48
58
 
49
59
  /**
50
60
  * Default Maximum Authentication Age. Specifies that the End-User MUST be
@@ -1,10 +1,8 @@
1
1
  import { z } from 'zod'
2
- import { ALLOW_UNSECURE_ORIGINS } from './constants.js'
3
- import { safeUrl } from './util.js'
2
+ import { webUriSchema } from './uri.js'
4
3
 
5
- export const oauthIssuerIdentifierSchema = z
6
- .string()
7
- .superRefine((value, ctx) => {
4
+ export const oauthIssuerIdentifierSchema = webUriSchema.superRefine(
5
+ (value, ctx) => {
8
6
  // Validate the issuer (MIX-UP attacks)
9
7
 
10
8
  if (value.endsWith('/')) {
@@ -12,32 +10,17 @@ export const oauthIssuerIdentifierSchema = z
12
10
  code: z.ZodIssueCode.custom,
13
11
  message: 'Issuer URL must not end with a slash',
14
12
  })
13
+ return false
15
14
  }
16
15
 
17
- const url = safeUrl(value)
18
- if (!url) {
19
- return ctx.addIssue({
20
- code: z.ZodIssueCode.custom,
21
- message: 'Invalid url',
22
- })
23
- }
24
-
25
- if (url.protocol !== 'https:') {
26
- if (ALLOW_UNSECURE_ORIGINS && url.protocol === 'http:') {
27
- // We'll allow HTTP in development mode
28
- } else {
29
- ctx.addIssue({
30
- code: z.ZodIssueCode.custom,
31
- message: 'Issuer must be an HTTPS URL',
32
- })
33
- }
34
- }
16
+ const url = new URL(value)
35
17
 
36
18
  if (url.username || url.password) {
37
19
  ctx.addIssue({
38
20
  code: z.ZodIssueCode.custom,
39
21
  message: 'Issuer URL must not contain a username or password',
40
22
  })
23
+ return false
41
24
  }
42
25
 
43
26
  if (url.hash || url.search) {
@@ -45,6 +28,7 @@ export const oauthIssuerIdentifierSchema = z
45
28
  code: z.ZodIssueCode.custom,
46
29
  message: 'Issuer URL must not contain a query or fragment',
47
30
  })
31
+ return false
48
32
  }
49
33
 
50
34
  const canonicalValue = url.pathname === '/' ? url.origin : url.href
@@ -53,5 +37,11 @@ export const oauthIssuerIdentifierSchema = z
53
37
  code: z.ZodIssueCode.custom,
54
38
  message: 'Issuer URL must be in the canonical form',
55
39
  })
40
+ return false
56
41
  }
57
- })
42
+
43
+ return true
44
+ },
45
+ )
46
+
47
+ export type OAuthIssuerIdentifier = z.infer<typeof oauthIssuerIdentifierSchema>
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  import { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js'
4
+ import { webUriSchema } from './uri.js'
4
5
 
5
6
  /**
6
7
  * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-05#name-protected-resource-metadata-r}
@@ -10,8 +11,17 @@ export const oauthProtectedResourceMetadataSchema = z.object({
10
11
  * REQUIRED. The protected resource's resource identifier, which is a URL that
11
12
  * uses the https scheme and has no query or fragment components. Using these
12
13
  * well-known resources is described in Section 3.
14
+ *
15
+ * @note This schema allows non https URLs for testing & development purposes.
16
+ * Make sure to validate the URL before using it in a production environment.
13
17
  */
14
- resource: z.string().url(),
18
+ resource: webUriSchema
19
+ .refine((url) => !url.includes('?'), {
20
+ message: 'Resource URL must not contain query parameters',
21
+ })
22
+ .refine((url) => !url.includes('#'), {
23
+ message: 'Resource URL must not contain a fragment',
24
+ }),
15
25
 
16
26
  /**
17
27
  * OPTIONAL. JSON array containing a list of OAuth authorization server issuer
@@ -31,7 +41,7 @@ export const oauthProtectedResourceMetadataSchema = z.object({
31
41
  * available, a use (public key use) parameter value is REQUIRED for all keys
32
42
  * in the referenced JWK Set to indicate each key's intended usage.
33
43
  */
34
- jwks_uri: z.string().url().optional(),
44
+ jwks_uri: webUriSchema.optional(),
35
45
 
36
46
  /**
37
47
  * RECOMMENDED. JSON array containing a list of the OAuth 2.0 [RFC6749] scope
@@ -64,20 +74,20 @@ export const oauthProtectedResourceMetadataSchema = z.object({
64
74
  * OPTIONAL. URL of a page containing human-readable information that
65
75
  * developers might want or need to know when using the protected resource
66
76
  */
67
- resource_documentation: z.string().url().optional(),
77
+ resource_documentation: webUriSchema.optional(),
68
78
 
69
79
  /**
70
80
  * OPTIONAL. URL that the protected resource provides to read about the
71
81
  * protected resource's requirements on how the client can use the data
72
82
  * provided by the protected resource
73
83
  */
74
- resource_policy_uri: z.string().url().optional(),
84
+ resource_policy_uri: webUriSchema.optional(),
75
85
 
76
86
  /**
77
87
  * OPTIONAL. URL that the protected resource provides to read about the
78
88
  * protected resource's terms of service
79
89
  */
80
- resource_tos_uri: z.string().url().optional(),
90
+ resource_tos_uri: webUriSchema.optional(),
81
91
  })
82
92
 
83
93
  export type OAuthProtectedResourceMetadata = z.infer<
@@ -0,0 +1,56 @@
1
+ import { TypeOf, z, ZodIssueCode } from 'zod'
2
+ import {
3
+ httpsUriSchema,
4
+ LoopbackUri,
5
+ loopbackUriSchema,
6
+ privateUseUriSchema,
7
+ } from './uri.js'
8
+
9
+ export const oauthLoopbackRedirectURISchema = loopbackUriSchema.superRefine(
10
+ (value, ctx): value is Exclude<LoopbackUri, `http://localhost${string}`> => {
11
+ if (value.startsWith('http://localhost')) {
12
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-8.3
13
+ //
14
+ // > While redirect URIs using localhost (i.e.,
15
+ // > "http://localhost:{port}/{path}") function similarly to loopback IP
16
+ // > redirects described in Section 7.3, the use of localhost is NOT
17
+ // > RECOMMENDED. Specifying a redirect URI with the loopback IP literal
18
+ // > rather than localhost avoids inadvertently listening on network
19
+ // > interfaces other than the loopback interface. It is also less
20
+ // > susceptible to client-side firewalls and misconfigured host name
21
+ // > resolution on the user's device.
22
+ ctx.addIssue({
23
+ code: ZodIssueCode.custom,
24
+ message:
25
+ 'Use of "localhost" hostname is not allowed (RFC 8252), use a loopback IP such as "127.0.0.1" instead',
26
+ })
27
+ return false
28
+ }
29
+
30
+ return true
31
+ },
32
+ )
33
+ export type OAuthLoopbackRedirectURI = TypeOf<
34
+ typeof oauthLoopbackRedirectURISchema
35
+ >
36
+
37
+ export const oauthHttpsRedirectURISchema = httpsUriSchema
38
+ export type OAuthHttpsRedirectURI = TypeOf<typeof oauthHttpsRedirectURISchema>
39
+
40
+ export const oauthPrivateUseRedirectURISchema = privateUseUriSchema
41
+ export type OAuthPrivateUseRedirectURI = TypeOf<
42
+ typeof oauthPrivateUseRedirectURISchema
43
+ >
44
+
45
+ export const oauthRedirectUriSchema = z.union(
46
+ [
47
+ oauthLoopbackRedirectURISchema,
48
+ oauthHttpsRedirectURISchema,
49
+ oauthPrivateUseRedirectURISchema,
50
+ ],
51
+ {
52
+ message: `URL must use the "https:" or "http:" protocol, or a private-use URI scheme (RFC 8252)`,
53
+ },
54
+ )
55
+
56
+ export type OAuthRedirectUri = TypeOf<typeof oauthRedirectUriSchema>
@@ -1,11 +1,9 @@
1
1
  import { z } from 'zod'
2
- import { oauthClientIdSchema } from './oauth-client-id.js'
3
2
  import { oauthRefreshTokenSchema } from './oauth-refresh-token.js'
4
3
 
5
4
  export const oauthRefreshTokenGrantTokenRequestSchema = z.object({
6
5
  grant_type: z.literal('refresh_token'),
7
6
  refresh_token: oauthRefreshTokenSchema,
8
- client_id: oauthClientIdSchema,
9
7
  })
10
8
 
11
9
  export type OAuthRefreshTokenGrantTokenRequest = z.infer<
@@ -9,13 +9,15 @@ import { oauthTokenTypeSchema } from './oauth-token-type.js'
9
9
  */
10
10
  export const oauthTokenResponseSchema = z
11
11
  .object({
12
+ // https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
12
13
  access_token: z.string(),
13
14
  token_type: oauthTokenTypeSchema,
14
- issuer: z.string().url().optional(),
15
15
  scope: z.string().optional(),
16
- id_token: signedJwtSchema.optional(),
17
16
  refresh_token: z.string().optional(),
18
17
  expires_in: z.number().optional(),
18
+ // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
19
+ id_token: signedJwtSchema.optional(),
20
+ // https://datatracker.ietf.org/doc/html/rfc9396#name-enriched-authorization-deta
19
21
  authorization_details: oauthAuthorizationDetailsSchema.optional(),
20
22
  })
21
23
  // https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
package/src/uri.ts ADDED
@@ -0,0 +1,171 @@
1
+ import { TypeOf, z, ZodIssueCode } from 'zod'
2
+ import { isHostnameIP, isLoopbackHost } from './util.js'
3
+
4
+ /**
5
+ * Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
6
+ *
7
+ * Any value that matches this schema is safe to parse using `new URL()`.
8
+ */
9
+ export const dangerousUriSchema = z
10
+ .string()
11
+ .refine(
12
+ (data): data is `${string}:${string}` =>
13
+ data.includes(':') && URL.canParse(data),
14
+ {
15
+ message: 'Invalid URL',
16
+ },
17
+ )
18
+
19
+ /**
20
+ * Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
21
+ */
22
+ export type DangerousUrl = TypeOf<typeof dangerousUriSchema>
23
+
24
+ export const loopbackUriSchema = dangerousUriSchema.superRefine(
25
+ (
26
+ value,
27
+ ctx,
28
+ ): value is
29
+ | `http://[::1]${string}`
30
+ | `http://localhost${'' | `${':' | '/' | '?' | '#'}${string}`}`
31
+ | `http://127.0.0.1${'' | `${':' | '/' | '?' | '#'}${string}`}` => {
32
+ // Loopback url must use the "http:" protocol
33
+ if (!value.startsWith('http://')) {
34
+ ctx.addIssue({
35
+ code: ZodIssueCode.custom,
36
+ message: 'URL must use the "http:" protocol',
37
+ })
38
+ return false
39
+ }
40
+
41
+ const url = new URL(value)
42
+
43
+ if (!isLoopbackHost(url.hostname)) {
44
+ ctx.addIssue({
45
+ code: ZodIssueCode.custom,
46
+ message: 'URL must use "localhost", "127.0.0.1" or "[::1]" as hostname',
47
+ })
48
+ return false
49
+ }
50
+
51
+ return true
52
+ },
53
+ )
54
+
55
+ export type LoopbackUri = TypeOf<typeof loopbackUriSchema>
56
+
57
+ export const httpsUriSchema = dangerousUriSchema.superRefine(
58
+ (value, ctx): value is `https://${string}` => {
59
+ if (!value.startsWith('https://')) {
60
+ ctx.addIssue({
61
+ code: ZodIssueCode.custom,
62
+ message: 'URL must use the "https:" protocol',
63
+ })
64
+ return false
65
+ }
66
+
67
+ const url = new URL(value)
68
+
69
+ // Disallow loopback URLs with the `https:` protocol
70
+ if (isLoopbackHost(url.hostname)) {
71
+ ctx.addIssue({
72
+ code: ZodIssueCode.custom,
73
+ message: 'https: URL must not use a loopback host',
74
+ })
75
+ return false
76
+ }
77
+
78
+ if (isHostnameIP(url.hostname)) {
79
+ // Hostname is an IP address
80
+ } else {
81
+ // Hostname is a domain name
82
+ if (!url.hostname.includes('.')) {
83
+ // we don't depend on PSL here, so we only check for a dot
84
+ ctx.addIssue({
85
+ code: ZodIssueCode.custom,
86
+ message: 'Domain name must contain at least two segments',
87
+ })
88
+ return false
89
+ }
90
+
91
+ if (url.hostname.endsWith('.local')) {
92
+ ctx.addIssue({
93
+ code: ZodIssueCode.custom,
94
+ message: 'Domain name must not end with ".local"',
95
+ })
96
+ return false
97
+ }
98
+ }
99
+
100
+ return true
101
+ },
102
+ )
103
+
104
+ export type HttpsUri = TypeOf<typeof httpsUriSchema>
105
+
106
+ export const webUriSchema = z
107
+ .string()
108
+ .superRefine((value, ctx): value is LoopbackUri | HttpsUri => {
109
+ // discriminated union of `loopbackUriSchema` and `httpsUriSchema`
110
+ if (value.startsWith('http://')) {
111
+ const result = loopbackUriSchema.safeParse(value)
112
+ if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)
113
+ return result.success
114
+ }
115
+
116
+ if (value.startsWith('https://')) {
117
+ const result = httpsUriSchema.safeParse(value)
118
+ if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)
119
+ return result.success
120
+ }
121
+
122
+ ctx.addIssue({
123
+ code: ZodIssueCode.custom,
124
+ message: 'URL must use the "http:" or "https:" protocol',
125
+ })
126
+ return false
127
+ })
128
+
129
+ export type WebUri = TypeOf<typeof webUriSchema>
130
+
131
+ export const privateUseUriSchema = dangerousUriSchema.superRefine(
132
+ (value, ctx): value is `${string}.${string}:/${string}` => {
133
+ const dotIdx = value.indexOf('.')
134
+ const colonIdx = value.indexOf(':')
135
+
136
+ // Optimization: avoid parsing the URL if the protocol does not contain a "."
137
+ if (dotIdx === -1 || colonIdx === -1 || dotIdx > colonIdx) {
138
+ ctx.addIssue({
139
+ code: ZodIssueCode.custom,
140
+ message:
141
+ 'Private-use URI scheme requires a "." as part of the protocol',
142
+ })
143
+ return false
144
+ }
145
+
146
+ const url = new URL(value)
147
+
148
+ // Should be covered by the check before, but let's be extra sure
149
+ if (!url.protocol.includes('.')) {
150
+ ctx.addIssue({
151
+ code: ZodIssueCode.custom,
152
+ message: 'Invalid private-use URI scheme',
153
+ })
154
+ return false
155
+ }
156
+
157
+ if (url.hostname) {
158
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
159
+ ctx.addIssue({
160
+ code: ZodIssueCode.custom,
161
+ message:
162
+ 'Private-use URI schemes must not include a hostname (only one "/" is allowed after the protocol, as per RFC 8252)',
163
+ })
164
+ return false
165
+ }
166
+
167
+ return true
168
+ },
169
+ )
170
+
171
+ export type PrivateUseUri = TypeOf<typeof privateUseUriSchema>
@@ -0,0 +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-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-claims-parameter.ts","./src/oidc-claims-properties.ts","./src/oidc-entity-type.ts","./src/uri.ts","./src/util.ts"],"version":"5.6.3"}