@atproto/oauth-types 0.1.5 → 0.2.1

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 (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"}