@atproto/oauth-types 0.7.3 → 0.7.5
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 +24 -0
- package/dist/atproto-loopback-client-id.d.ts +3 -3
- package/dist/atproto-loopback-client-id.d.ts.map +1 -1
- package/dist/atproto-loopback-client-id.js.map +1 -1
- package/dist/atproto-loopback-client-metadata.d.ts +5 -5
- package/dist/atproto-loopback-client-metadata.d.ts.map +1 -1
- package/dist/atproto-loopback-client-metadata.js.map +1 -1
- package/dist/atproto-oauth-scope.d.ts +2 -2
- package/dist/atproto-oauth-scope.d.ts.map +1 -1
- package/dist/atproto-oauth-scope.js.map +1 -1
- package/dist/atproto-oauth-token-response.d.ts +1 -1
- package/dist/atproto-oauth-token-response.d.ts.map +1 -1
- package/dist/atproto-oauth-token-response.js.map +1 -1
- package/dist/oauth-client-id-discoverable.d.ts +1 -1
- package/dist/oauth-client-id-discoverable.d.ts.map +1 -1
- package/dist/oauth-client-id-discoverable.js.map +1 -1
- package/dist/oauth-client-id-loopback.d.ts +2 -2
- package/dist/oauth-client-id-loopback.d.ts.map +1 -1
- package/dist/oauth-client-id-loopback.js.map +1 -1
- package/dist/oauth-introspection-response.d.ts +2 -2
- package/dist/oauth-introspection-response.d.ts.map +1 -1
- package/dist/oauth-introspection-response.js.map +1 -1
- package/dist/oauth-redirect-uri.d.ts +1 -1
- package/dist/oauth-redirect-uri.d.ts.map +1 -1
- package/dist/oauth-redirect-uri.js.map +1 -1
- package/dist/uri.d.ts +1 -1
- package/dist/uri.d.ts.map +1 -1
- package/dist/uri.js.map +1 -1
- package/dist/util.d.ts +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js.map +1 -1
- package/package.json +11 -7
- package/src/atproto-loopback-client-id.ts +0 -78
- package/src/atproto-loopback-client-metadata.ts +0 -47
- package/src/atproto-loopback-client-redirect-uris.ts +0 -4
- package/src/atproto-oauth-scope.ts +0 -34
- package/src/atproto-oauth-token-response.ts +0 -16
- package/src/constants.ts +0 -2
- package/src/index.ts +0 -50
- package/src/oauth-access-token.ts +0 -4
- package/src/oauth-authorization-code-grant-token-request.ts +0 -19
- package/src/oauth-authorization-details.ts +0 -48
- package/src/oauth-authorization-request-jar.ts +0 -16
- package/src/oauth-authorization-request-par.ts +0 -12
- package/src/oauth-authorization-request-parameters.ts +0 -100
- package/src/oauth-authorization-request-query.ts +0 -21
- package/src/oauth-authorization-request-uri.ts +0 -10
- package/src/oauth-authorization-response-error.ts +0 -25
- package/src/oauth-authorization-server-metadata.ts +0 -120
- package/src/oauth-client-credentials-grant-token-request.ts +0 -9
- package/src/oauth-client-credentials.ts +0 -53
- package/src/oauth-client-id-discoverable.ts +0 -130
- package/src/oauth-client-id-loopback.ts +0 -164
- package/src/oauth-client-id.ts +0 -4
- package/src/oauth-client-metadata.ts +0 -80
- package/src/oauth-code-challenge-method.ts +0 -3
- package/src/oauth-endpoint-auth-method.ts +0 -13
- package/src/oauth-endpoint-name.ts +0 -8
- package/src/oauth-grant-type.ts +0 -13
- package/src/oauth-introspection-response.ts +0 -23
- package/src/oauth-issuer-identifier.ts +0 -47
- package/src/oauth-par-response.ts +0 -8
- package/src/oauth-password-grant-token-request.ts +0 -11
- package/src/oauth-prompt-mode.ts +0 -18
- package/src/oauth-protected-resource-metadata.ts +0 -94
- package/src/oauth-redirect-uri.ts +0 -73
- package/src/oauth-refresh-token-grant-token-request.ts +0 -11
- package/src/oauth-refresh-token.ts +0 -4
- package/src/oauth-request-uri.ts +0 -5
- package/src/oauth-response-mode.ts +0 -9
- package/src/oauth-response-type.ts +0 -17
- package/src/oauth-scope.ts +0 -21
- package/src/oauth-token-identification.ts +0 -12
- package/src/oauth-token-request.ts +0 -14
- package/src/oauth-token-response.ts +0 -29
- package/src/oauth-token-type.ts +0 -15
- package/src/oidc-authorization-error-response.ts +0 -29
- package/src/oidc-claims-parameter.ts +0 -40
- package/src/oidc-claims-properties.ts +0 -11
- package/src/oidc-entity-type.ts +0 -5
- package/src/oidc-userinfo.ts +0 -15
- package/src/uri.ts +0 -221
- package/src/util.ts +0 -170
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -4
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { TypeOf, ZodIssueCode, z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
HttpsUri,
|
|
4
|
-
LoopbackUri,
|
|
5
|
-
PrivateUseUri,
|
|
6
|
-
httpsUriSchema,
|
|
7
|
-
loopbackUriSchema,
|
|
8
|
-
privateUseUriSchema,
|
|
9
|
-
} from './uri.js'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* This is a {@link loopbackUriSchema} with the additional restriction that
|
|
13
|
-
* the hostname `localhost` is not allowed.
|
|
14
|
-
*
|
|
15
|
-
* @see {@link https://datatracker.ietf.org/doc/html/rfc8252#section-8.3 Loopback Redirect Considerations} RFC8252
|
|
16
|
-
*
|
|
17
|
-
* > While redirect URIs using localhost (i.e.,
|
|
18
|
-
* > "http://localhost:{port}/{path}") function similarly to loopback IP
|
|
19
|
-
* > redirects described in Section 7.3, the use of localhost is NOT
|
|
20
|
-
* > RECOMMENDED. Specifying a redirect URI with the loopback IP literal rather
|
|
21
|
-
* > than localhost avoids inadvertently listening on network interfaces other
|
|
22
|
-
* > than the loopback interface. It is also less susceptible to client-side
|
|
23
|
-
* > firewalls and misconfigured host name resolution on the user's device.
|
|
24
|
-
*/
|
|
25
|
-
export const loopbackRedirectURISchema = loopbackUriSchema.superRefine(
|
|
26
|
-
(value, ctx): value is Exclude<LoopbackUri, `http://localhost${string}`> => {
|
|
27
|
-
if (value.startsWith('http://localhost')) {
|
|
28
|
-
ctx.addIssue({
|
|
29
|
-
code: ZodIssueCode.custom,
|
|
30
|
-
message:
|
|
31
|
-
'Use of "localhost" hostname is not allowed (RFC 8252), use a loopback IP such as "127.0.0.1" instead',
|
|
32
|
-
})
|
|
33
|
-
return false
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return true
|
|
37
|
-
},
|
|
38
|
-
)
|
|
39
|
-
export type LoopbackRedirectURI = TypeOf<typeof loopbackRedirectURISchema>
|
|
40
|
-
|
|
41
|
-
export const oauthLoopbackClientRedirectUriSchema = loopbackRedirectURISchema
|
|
42
|
-
export type OAuthLoopbackRedirectURI = TypeOf<
|
|
43
|
-
typeof oauthLoopbackClientRedirectUriSchema
|
|
44
|
-
>
|
|
45
|
-
|
|
46
|
-
export const oauthRedirectUriSchema = z
|
|
47
|
-
.string()
|
|
48
|
-
.superRefine(
|
|
49
|
-
(value, ctx): value is HttpsUri | LoopbackRedirectURI | PrivateUseUri => {
|
|
50
|
-
if (value.startsWith('https:')) {
|
|
51
|
-
const result = httpsUriSchema.safeParse(value)
|
|
52
|
-
if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)
|
|
53
|
-
return result.success
|
|
54
|
-
} else if (value.startsWith('http:')) {
|
|
55
|
-
const result = loopbackRedirectURISchema.safeParse(value)
|
|
56
|
-
if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)
|
|
57
|
-
return result.success
|
|
58
|
-
} else if (/^[^.:]+(?:\.[^.:]+)+:/.test(value)) {
|
|
59
|
-
const result = privateUseUriSchema.safeParse(value)
|
|
60
|
-
if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)
|
|
61
|
-
return result.success
|
|
62
|
-
} else {
|
|
63
|
-
ctx.addIssue({
|
|
64
|
-
code: ZodIssueCode.custom,
|
|
65
|
-
message:
|
|
66
|
-
'URL must use the "https:" or "http:" protocol, or a private-use URI scheme (RFC 8252)',
|
|
67
|
-
})
|
|
68
|
-
return false
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
export type OAuthRedirectUri = TypeOf<typeof oauthRedirectUriSchema>
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { oauthRefreshTokenSchema } from './oauth-refresh-token.js'
|
|
3
|
-
|
|
4
|
-
export const oauthRefreshTokenGrantTokenRequestSchema = z.object({
|
|
5
|
-
grant_type: z.literal('refresh_token'),
|
|
6
|
-
refresh_token: oauthRefreshTokenSchema,
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
export type OAuthRefreshTokenGrantTokenRequest = z.infer<
|
|
10
|
-
typeof oauthRefreshTokenGrantTokenRequestSchema
|
|
11
|
-
>
|
package/src/oauth-request-uri.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
export const oauthResponseTypeSchema = z.enum([
|
|
4
|
-
// OAuth2 (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-4.1.1)
|
|
5
|
-
'code', // Authorization Code Grant
|
|
6
|
-
'token', // Implicit Grant
|
|
7
|
-
|
|
8
|
-
// OIDC (https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)
|
|
9
|
-
'none',
|
|
10
|
-
'code id_token token',
|
|
11
|
-
'code id_token',
|
|
12
|
-
'code token',
|
|
13
|
-
'id_token token',
|
|
14
|
-
'id_token',
|
|
15
|
-
])
|
|
16
|
-
|
|
17
|
-
export type OAuthResponseType = z.infer<typeof oauthResponseTypeSchema>
|
package/src/oauth-scope.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
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
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A (single) space separated list of non empty printable ASCII char string
|
|
13
|
-
* (except backslash and double quote).
|
|
14
|
-
*
|
|
15
|
-
* @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-1.4.1}
|
|
16
|
-
*/
|
|
17
|
-
export const oauthScopeSchema = z.string().refine(isOAuthScope, {
|
|
18
|
-
message: 'Invalid OAuth scope',
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
export type OAuthScope = z.infer<typeof oauthScopeSchema>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { oauthAccessTokenSchema } from './oauth-access-token.js'
|
|
3
|
-
import { oauthRefreshTokenSchema } from './oauth-refresh-token.js'
|
|
4
|
-
|
|
5
|
-
export const oauthTokenIdentificationSchema = z.object({
|
|
6
|
-
token: z.union([oauthAccessTokenSchema, oauthRefreshTokenSchema]),
|
|
7
|
-
token_type_hint: z.enum(['access_token', 'refresh_token']).optional(),
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
export type OAuthTokenIdentification = z.infer<
|
|
11
|
-
typeof oauthTokenIdentificationSchema
|
|
12
|
-
>
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { oauthAuthorizationCodeGrantTokenRequestSchema } from './oauth-authorization-code-grant-token-request.js'
|
|
3
|
-
import { oauthClientCredentialsGrantTokenRequestSchema } from './oauth-client-credentials-grant-token-request.js'
|
|
4
|
-
import { oauthPasswordGrantTokenRequestSchema } from './oauth-password-grant-token-request.js'
|
|
5
|
-
import { oauthRefreshTokenGrantTokenRequestSchema } from './oauth-refresh-token-grant-token-request.js'
|
|
6
|
-
|
|
7
|
-
export const oauthTokenRequestSchema = z.discriminatedUnion('grant_type', [
|
|
8
|
-
oauthAuthorizationCodeGrantTokenRequestSchema,
|
|
9
|
-
oauthRefreshTokenGrantTokenRequestSchema,
|
|
10
|
-
oauthPasswordGrantTokenRequestSchema,
|
|
11
|
-
oauthClientCredentialsGrantTokenRequestSchema,
|
|
12
|
-
])
|
|
13
|
-
|
|
14
|
-
export type OAuthTokenRequest = z.infer<typeof oauthTokenRequestSchema>
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { signedJwtSchema } from '@atproto/jwk'
|
|
3
|
-
import { oauthAuthorizationDetailsSchema } from './oauth-authorization-details.js'
|
|
4
|
-
import { oauthTokenTypeSchema } from './oauth-token-type.js'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @see {@link https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1 | RFC 6749 (OAuth2), Section 5.1}
|
|
8
|
-
*/
|
|
9
|
-
export const oauthTokenResponseSchema = z
|
|
10
|
-
.object({
|
|
11
|
-
// https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
|
|
12
|
-
access_token: z.string(),
|
|
13
|
-
token_type: oauthTokenTypeSchema,
|
|
14
|
-
scope: z.string().optional(),
|
|
15
|
-
refresh_token: z.string().optional(),
|
|
16
|
-
expires_in: z.number().optional(),
|
|
17
|
-
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
|
|
18
|
-
id_token: signedJwtSchema.optional(),
|
|
19
|
-
// https://datatracker.ietf.org/doc/html/rfc9396#name-enriched-authorization-deta
|
|
20
|
-
authorization_details: oauthAuthorizationDetailsSchema.optional(),
|
|
21
|
-
})
|
|
22
|
-
// https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
|
|
23
|
-
// > The client MUST ignore unrecognized value names in the response.
|
|
24
|
-
.passthrough()
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @see {@link oauthTokenResponseSchema}
|
|
28
|
-
*/
|
|
29
|
-
export type OAuthTokenResponse = z.infer<typeof oauthTokenResponseSchema>
|
package/src/oauth-token-type.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
// Case insensitive input, normalized output
|
|
4
|
-
export const oauthTokenTypeSchema = z.union([
|
|
5
|
-
z
|
|
6
|
-
.string()
|
|
7
|
-
.regex(/^DPoP$/i)
|
|
8
|
-
.transform(() => 'DPoP' as const),
|
|
9
|
-
z
|
|
10
|
-
.string()
|
|
11
|
-
.regex(/^Bearer$/i)
|
|
12
|
-
.transform(() => 'Bearer' as const),
|
|
13
|
-
])
|
|
14
|
-
|
|
15
|
-
export type OAuthTokenType = z.infer<typeof oauthTokenTypeSchema>
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthError}
|
|
5
|
-
*/
|
|
6
|
-
export const oidcAuthorizationResponseErrorSchema = z.enum([
|
|
7
|
-
// The Authorization Server requires End-User interaction of some form to proceed. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for End-User interaction.
|
|
8
|
-
'interaction_required',
|
|
9
|
-
// The Authorization Server requires End-User authentication. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for End-User authentication.
|
|
10
|
-
'login_required',
|
|
11
|
-
// The End-User is REQUIRED to select a session at the Authorization Server. The End-User MAY be authenticated at the Authorization Server with different associated accounts, but the End-User did not select a session. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface to prompt for a session to use.
|
|
12
|
-
'account_selection_required',
|
|
13
|
-
// The Authorization Server requires End-User consent. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for End-User consent.
|
|
14
|
-
'consent_required',
|
|
15
|
-
// The request_uri in the Authorization Request returns an error or contains invalid data.
|
|
16
|
-
'invalid_request_uri',
|
|
17
|
-
// The request parameter contains an invalid Request Object.
|
|
18
|
-
'invalid_request_object',
|
|
19
|
-
// The OP does not support use of the request parameter defined in Section 6.
|
|
20
|
-
'request_not_supported',
|
|
21
|
-
// The OP does not support use of the request_uri parameter defined in Section 6.
|
|
22
|
-
'request_uri_not_supported',
|
|
23
|
-
// The OP does not support use of the registration parameter defined in Section 7.2.1.
|
|
24
|
-
'registration_not_supported',
|
|
25
|
-
])
|
|
26
|
-
|
|
27
|
-
export type OidcAuthorizationResponseError = z.infer<
|
|
28
|
-
typeof oidcAuthorizationResponseErrorSchema
|
|
29
|
-
>
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
export const oidcClaimsParameterSchema = z.enum([
|
|
4
|
-
// https://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html#rfc.section.5.2
|
|
5
|
-
// if client metadata "require_auth_time" is true, this *must* be provided
|
|
6
|
-
'auth_time',
|
|
7
|
-
|
|
8
|
-
// OIDC
|
|
9
|
-
'nonce',
|
|
10
|
-
'acr',
|
|
11
|
-
|
|
12
|
-
// OpenID: "profile" scope
|
|
13
|
-
'name',
|
|
14
|
-
'family_name',
|
|
15
|
-
'given_name',
|
|
16
|
-
'middle_name',
|
|
17
|
-
'nickname',
|
|
18
|
-
'preferred_username',
|
|
19
|
-
'gender',
|
|
20
|
-
'picture',
|
|
21
|
-
'profile',
|
|
22
|
-
'website',
|
|
23
|
-
'birthdate',
|
|
24
|
-
'zoneinfo',
|
|
25
|
-
'locale',
|
|
26
|
-
'updated_at',
|
|
27
|
-
|
|
28
|
-
// OpenID: "email" scope
|
|
29
|
-
'email',
|
|
30
|
-
'email_verified',
|
|
31
|
-
|
|
32
|
-
// OpenID: "phone" scope
|
|
33
|
-
'phone_number',
|
|
34
|
-
'phone_number_verified',
|
|
35
|
-
|
|
36
|
-
// OpenID: "address" scope
|
|
37
|
-
'address',
|
|
38
|
-
])
|
|
39
|
-
|
|
40
|
-
export type OidcClaimsParameter = z.infer<typeof oidcClaimsParameterSchema>
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
const oidcClaimsValueSchema = z.union([z.string(), z.number(), z.boolean()])
|
|
4
|
-
|
|
5
|
-
export const oidcClaimsPropertiesSchema = z.object({
|
|
6
|
-
essential: z.boolean().optional(),
|
|
7
|
-
value: oidcClaimsValueSchema.optional(),
|
|
8
|
-
values: z.array(oidcClaimsValueSchema).optional(),
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
export type OidcClaimsProperties = z.infer<typeof oidcClaimsPropertiesSchema>
|
package/src/oidc-entity-type.ts
DELETED
package/src/oidc-userinfo.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
export const oidcUserinfoSchema = z.object({
|
|
4
|
-
sub: z.string(),
|
|
5
|
-
iss: z.string().url().optional(),
|
|
6
|
-
aud: z.union([z.string(), z.array(z.string()).min(1)]).optional(),
|
|
7
|
-
|
|
8
|
-
email: z.string().email().optional(),
|
|
9
|
-
email_verified: z.boolean().optional(),
|
|
10
|
-
name: z.string().optional(),
|
|
11
|
-
preferred_username: z.string().optional(),
|
|
12
|
-
picture: z.string().url().optional(),
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
export type OidcUserinfo = z.infer<typeof oidcUserinfoSchema>
|
package/src/uri.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { TypeOf, ZodIssueCode, z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
canParseUrl,
|
|
4
|
-
isHostnameIP,
|
|
5
|
-
isLocalHostname,
|
|
6
|
-
isLoopbackHost,
|
|
7
|
-
} from './util.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
|
|
11
|
-
*
|
|
12
|
-
* Any value that matches this schema is safe to parse using `new URL()`.
|
|
13
|
-
*/
|
|
14
|
-
export const dangerousUriSchema = z
|
|
15
|
-
.string()
|
|
16
|
-
.refine(
|
|
17
|
-
(data): data is `${string}:${string}` =>
|
|
18
|
-
data.includes(':') && canParseUrl(data),
|
|
19
|
-
{
|
|
20
|
-
message: 'Invalid URL',
|
|
21
|
-
},
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
|
|
26
|
-
*/
|
|
27
|
-
export type DangerousUrl = TypeOf<typeof dangerousUriSchema>
|
|
28
|
-
|
|
29
|
-
export const loopbackUriSchema = dangerousUriSchema.superRefine(
|
|
30
|
-
(
|
|
31
|
-
value,
|
|
32
|
-
ctx,
|
|
33
|
-
): value is
|
|
34
|
-
| `http://[::1]${string}`
|
|
35
|
-
| `http://localhost${'' | `${':' | '/' | '?' | '#'}${string}`}`
|
|
36
|
-
| `http://127.0.0.1${'' | `${':' | '/' | '?' | '#'}${string}`}` => {
|
|
37
|
-
// Loopback url must use the "http:" protocol
|
|
38
|
-
if (!value.startsWith('http://')) {
|
|
39
|
-
ctx.addIssue({
|
|
40
|
-
code: ZodIssueCode.custom,
|
|
41
|
-
message: 'URL must use the "http:" protocol',
|
|
42
|
-
})
|
|
43
|
-
return false
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const url = new URL(value)
|
|
47
|
-
|
|
48
|
-
if (!isLoopbackHost(url.hostname)) {
|
|
49
|
-
ctx.addIssue({
|
|
50
|
-
code: ZodIssueCode.custom,
|
|
51
|
-
message: 'URL must use "localhost", "127.0.0.1" or "[::1]" as hostname',
|
|
52
|
-
})
|
|
53
|
-
return false
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return true
|
|
57
|
-
},
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
export type LoopbackUri = TypeOf<typeof loopbackUriSchema>
|
|
61
|
-
|
|
62
|
-
export const httpsUriSchema = dangerousUriSchema.superRefine(
|
|
63
|
-
(value, ctx): value is `https://${string}` => {
|
|
64
|
-
if (!value.startsWith('https://')) {
|
|
65
|
-
ctx.addIssue({
|
|
66
|
-
code: ZodIssueCode.custom,
|
|
67
|
-
message: 'URL must use the "https:" protocol',
|
|
68
|
-
})
|
|
69
|
-
return false
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const url = new URL(value)
|
|
73
|
-
|
|
74
|
-
// Disallow loopback URLs with the `https:` protocol
|
|
75
|
-
if (isLoopbackHost(url.hostname)) {
|
|
76
|
-
ctx.addIssue({
|
|
77
|
-
code: ZodIssueCode.custom,
|
|
78
|
-
message: 'https: URL must not use a loopback host',
|
|
79
|
-
})
|
|
80
|
-
return false
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (isHostnameIP(url.hostname)) {
|
|
84
|
-
// Hostname is an IP address
|
|
85
|
-
} else {
|
|
86
|
-
// Hostname is a domain name
|
|
87
|
-
if (!url.hostname.includes('.')) {
|
|
88
|
-
// we don't depend on PSL here, so we only check for a dot
|
|
89
|
-
ctx.addIssue({
|
|
90
|
-
code: ZodIssueCode.custom,
|
|
91
|
-
message: 'Domain name must contain at least two segments',
|
|
92
|
-
})
|
|
93
|
-
return false
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (url.hostname.endsWith('.local')) {
|
|
97
|
-
ctx.addIssue({
|
|
98
|
-
code: ZodIssueCode.custom,
|
|
99
|
-
message: 'Domain name must not end with ".local"',
|
|
100
|
-
})
|
|
101
|
-
return false
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return true
|
|
106
|
-
},
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
export type HttpsUri = TypeOf<typeof httpsUriSchema>
|
|
110
|
-
|
|
111
|
-
export const webUriSchema = z
|
|
112
|
-
.string()
|
|
113
|
-
.superRefine((value, ctx): value is LoopbackUri | HttpsUri => {
|
|
114
|
-
// discriminated union of `loopbackUriSchema` and `httpsUriSchema`
|
|
115
|
-
if (value.startsWith('http://')) {
|
|
116
|
-
const result = loopbackUriSchema.safeParse(value)
|
|
117
|
-
if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)
|
|
118
|
-
return result.success
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (value.startsWith('https://')) {
|
|
122
|
-
const result = httpsUriSchema.safeParse(value)
|
|
123
|
-
if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)
|
|
124
|
-
return result.success
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
ctx.addIssue({
|
|
128
|
-
code: ZodIssueCode.custom,
|
|
129
|
-
message: 'URL must use the "http:" or "https:" protocol',
|
|
130
|
-
})
|
|
131
|
-
return false
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
export type WebUri = TypeOf<typeof webUriSchema>
|
|
135
|
-
|
|
136
|
-
export const privateUseUriSchema = dangerousUriSchema.superRefine(
|
|
137
|
-
(value, ctx): value is `${string}.${string}:/${string}` => {
|
|
138
|
-
const dotIdx = value.indexOf('.')
|
|
139
|
-
const colonIdx = value.indexOf(':')
|
|
140
|
-
|
|
141
|
-
// Optimization: avoid parsing the URL if the protocol does not contain a "."
|
|
142
|
-
if (dotIdx === -1 || colonIdx === -1 || dotIdx > colonIdx) {
|
|
143
|
-
ctx.addIssue({
|
|
144
|
-
code: ZodIssueCode.custom,
|
|
145
|
-
message:
|
|
146
|
-
'Private-use URI scheme requires a "." as part of the protocol',
|
|
147
|
-
})
|
|
148
|
-
return false
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const url = new URL(value)
|
|
152
|
-
|
|
153
|
-
// Should be covered by the check before, but let's be extra sure
|
|
154
|
-
if (!url.protocol.includes('.')) {
|
|
155
|
-
ctx.addIssue({
|
|
156
|
-
code: ZodIssueCode.custom,
|
|
157
|
-
message: 'Invalid private-use URI scheme',
|
|
158
|
-
})
|
|
159
|
-
return false
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
|
|
163
|
-
//
|
|
164
|
-
// > When choosing a URI scheme to associate with the app, apps MUST use a
|
|
165
|
-
// > URI scheme based on a domain name under their control, expressed in
|
|
166
|
-
// > reverse order
|
|
167
|
-
//
|
|
168
|
-
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
|
|
169
|
-
//
|
|
170
|
-
// > In addition to the collision-resistant properties, requiring a URI
|
|
171
|
-
// > scheme based on a domain name that is under the control of the app can
|
|
172
|
-
// > help to prove ownership in the event of a dispute where two apps claim
|
|
173
|
-
// > the same private-use URI scheme (where one app is acting maliciously).
|
|
174
|
-
//
|
|
175
|
-
// We can't check for ownership here (as there is no concept of proven
|
|
176
|
-
// ownership in a generic validation logic), besides excluding local domains
|
|
177
|
-
// as they can't be controlled/owned by the app.
|
|
178
|
-
//
|
|
179
|
-
// https://atproto.com/specs/oauth
|
|
180
|
-
//
|
|
181
|
-
// > Any custom scheme must match the `client_id` hostname in reverse-domain
|
|
182
|
-
// > order.
|
|
183
|
-
//
|
|
184
|
-
// This ATPROTO specific requirement cannot be enforced here, (as there is
|
|
185
|
-
// no concept of `client_id` in this context).
|
|
186
|
-
|
|
187
|
-
const uriScheme = url.protocol.slice(0, -1) // remove trailing ":"
|
|
188
|
-
const urlDomain = uriScheme.split('.').reverse().join('.')
|
|
189
|
-
|
|
190
|
-
if (isLocalHostname(urlDomain)) {
|
|
191
|
-
ctx.addIssue({
|
|
192
|
-
code: ZodIssueCode.custom,
|
|
193
|
-
message: `Private-use URI Scheme redirect URI must not be a local hostname`,
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
|
|
198
|
-
//
|
|
199
|
-
// > Following the requirements of Section 3.2 of [RFC3986], as there is no
|
|
200
|
-
// > naming authority for private-use URI scheme redirects, only a single
|
|
201
|
-
// > slash ("/") appears after the scheme component.
|
|
202
|
-
if (
|
|
203
|
-
url.href.startsWith(`${url.protocol}//`) ||
|
|
204
|
-
url.username ||
|
|
205
|
-
url.password ||
|
|
206
|
-
url.hostname ||
|
|
207
|
-
url.port
|
|
208
|
-
) {
|
|
209
|
-
ctx.addIssue({
|
|
210
|
-
code: ZodIssueCode.custom,
|
|
211
|
-
message:
|
|
212
|
-
'Private-Use URI Scheme must be in the form <scheme>:/{path} (notice the single slash!) as per RFC 8252',
|
|
213
|
-
})
|
|
214
|
-
return false
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return true
|
|
218
|
-
},
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
export type PrivateUseUri = TypeOf<typeof privateUseUriSchema>
|