feat: zod v4 with backwards compatibility for v3.25+ (#1040)
This commit is contained in:
parent
7fca1f2b15
commit
418b7f9b85
3 changed files with 14723 additions and 1002 deletions
|
|
@ -1,10 +1,9 @@
|
|||
import { z } from 'zod';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
/**
|
||||
* Reusable URL validation that disallows javascript: scheme
|
||||
*/
|
||||
export const SafeUrlSchema = z
|
||||
.string()
|
||||
.url()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!URL.canParse(val)) {
|
||||
|
|
@ -28,107 +27,102 @@ export const SafeUrlSchema = z
|
|||
/**
|
||||
* RFC 9728 OAuth Protected Resource Metadata
|
||||
*/
|
||||
export const OAuthProtectedResourceMetadataSchema = z
|
||||
.object({
|
||||
resource: z.string().url(),
|
||||
authorization_servers: z.array(SafeUrlSchema).optional(),
|
||||
jwks_uri: z.string().url().optional(),
|
||||
scopes_supported: z.array(z.string()).optional(),
|
||||
bearer_methods_supported: z.array(z.string()).optional(),
|
||||
resource_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
resource_name: z.string().optional(),
|
||||
resource_documentation: z.string().optional(),
|
||||
resource_policy_uri: z.string().url().optional(),
|
||||
resource_tos_uri: z.string().url().optional(),
|
||||
tls_client_certificate_bound_access_tokens: z.boolean().optional(),
|
||||
authorization_details_types_supported: z.array(z.string()).optional(),
|
||||
dpop_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
dpop_bound_access_tokens_required: z.boolean().optional()
|
||||
})
|
||||
.passthrough();
|
||||
export const OAuthProtectedResourceMetadataSchema = z.looseObject({
|
||||
resource: z.string().url(),
|
||||
authorization_servers: z.array(SafeUrlSchema).optional(),
|
||||
jwks_uri: z.string().url().optional(),
|
||||
scopes_supported: z.array(z.string()).optional(),
|
||||
bearer_methods_supported: z.array(z.string()).optional(),
|
||||
resource_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
resource_name: z.string().optional(),
|
||||
resource_documentation: z.string().optional(),
|
||||
resource_policy_uri: z.string().url().optional(),
|
||||
resource_tos_uri: z.string().url().optional(),
|
||||
tls_client_certificate_bound_access_tokens: z.boolean().optional(),
|
||||
authorization_details_types_supported: z.array(z.string()).optional(),
|
||||
dpop_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
dpop_bound_access_tokens_required: z.boolean().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* RFC 8414 OAuth 2.0 Authorization Server Metadata
|
||||
*/
|
||||
export const OAuthMetadataSchema = z
|
||||
.object({
|
||||
issuer: z.string(),
|
||||
authorization_endpoint: SafeUrlSchema,
|
||||
token_endpoint: SafeUrlSchema,
|
||||
registration_endpoint: SafeUrlSchema.optional(),
|
||||
scopes_supported: z.array(z.string()).optional(),
|
||||
response_types_supported: z.array(z.string()),
|
||||
response_modes_supported: z.array(z.string()).optional(),
|
||||
grant_types_supported: z.array(z.string()).optional(),
|
||||
token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
|
||||
token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
service_documentation: SafeUrlSchema.optional(),
|
||||
revocation_endpoint: SafeUrlSchema.optional(),
|
||||
revocation_endpoint_auth_methods_supported: z.array(z.string()).optional(),
|
||||
revocation_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
introspection_endpoint: z.string().optional(),
|
||||
introspection_endpoint_auth_methods_supported: z.array(z.string()).optional(),
|
||||
introspection_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
code_challenge_methods_supported: z.array(z.string()).optional(),
|
||||
client_id_metadata_document_supported: z.boolean().optional()
|
||||
})
|
||||
.passthrough();
|
||||
export const OAuthMetadataSchema = z.looseObject({
|
||||
issuer: z.string(),
|
||||
authorization_endpoint: SafeUrlSchema,
|
||||
token_endpoint: SafeUrlSchema,
|
||||
registration_endpoint: SafeUrlSchema.optional(),
|
||||
scopes_supported: z.array(z.string()).optional(),
|
||||
response_types_supported: z.array(z.string()),
|
||||
response_modes_supported: z.array(z.string()).optional(),
|
||||
grant_types_supported: z.array(z.string()).optional(),
|
||||
token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
|
||||
token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
service_documentation: SafeUrlSchema.optional(),
|
||||
revocation_endpoint: SafeUrlSchema.optional(),
|
||||
revocation_endpoint_auth_methods_supported: z.array(z.string()).optional(),
|
||||
revocation_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
introspection_endpoint: z.string().optional(),
|
||||
introspection_endpoint_auth_methods_supported: z.array(z.string()).optional(),
|
||||
introspection_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
code_challenge_methods_supported: z.array(z.string()).optional(),
|
||||
client_id_metadata_document_supported: z.boolean().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* OpenID Connect Discovery 1.0 Provider Metadata
|
||||
* see: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
*/
|
||||
export const OpenIdProviderMetadataSchema = z
|
||||
.object({
|
||||
issuer: z.string(),
|
||||
authorization_endpoint: SafeUrlSchema,
|
||||
token_endpoint: SafeUrlSchema,
|
||||
userinfo_endpoint: SafeUrlSchema.optional(),
|
||||
jwks_uri: SafeUrlSchema,
|
||||
registration_endpoint: SafeUrlSchema.optional(),
|
||||
scopes_supported: z.array(z.string()).optional(),
|
||||
response_types_supported: z.array(z.string()),
|
||||
response_modes_supported: z.array(z.string()).optional(),
|
||||
grant_types_supported: z.array(z.string()).optional(),
|
||||
acr_values_supported: z.array(z.string()).optional(),
|
||||
subject_types_supported: z.array(z.string()),
|
||||
id_token_signing_alg_values_supported: z.array(z.string()),
|
||||
id_token_encryption_alg_values_supported: z.array(z.string()).optional(),
|
||||
id_token_encryption_enc_values_supported: z.array(z.string()).optional(),
|
||||
userinfo_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
userinfo_encryption_alg_values_supported: z.array(z.string()).optional(),
|
||||
userinfo_encryption_enc_values_supported: z.array(z.string()).optional(),
|
||||
request_object_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
request_object_encryption_alg_values_supported: z.array(z.string()).optional(),
|
||||
request_object_encryption_enc_values_supported: z.array(z.string()).optional(),
|
||||
token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
|
||||
token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
display_values_supported: z.array(z.string()).optional(),
|
||||
claim_types_supported: z.array(z.string()).optional(),
|
||||
claims_supported: z.array(z.string()).optional(),
|
||||
service_documentation: z.string().optional(),
|
||||
claims_locales_supported: z.array(z.string()).optional(),
|
||||
ui_locales_supported: z.array(z.string()).optional(),
|
||||
claims_parameter_supported: z.boolean().optional(),
|
||||
request_parameter_supported: z.boolean().optional(),
|
||||
request_uri_parameter_supported: z.boolean().optional(),
|
||||
require_request_uri_registration: z.boolean().optional(),
|
||||
op_policy_uri: SafeUrlSchema.optional(),
|
||||
op_tos_uri: SafeUrlSchema.optional(),
|
||||
client_id_metadata_document_supported: z.boolean().optional()
|
||||
})
|
||||
.passthrough();
|
||||
export const OpenIdProviderMetadataSchema = z.looseObject({
|
||||
issuer: z.string(),
|
||||
authorization_endpoint: SafeUrlSchema,
|
||||
token_endpoint: SafeUrlSchema,
|
||||
userinfo_endpoint: SafeUrlSchema.optional(),
|
||||
jwks_uri: SafeUrlSchema,
|
||||
registration_endpoint: SafeUrlSchema.optional(),
|
||||
scopes_supported: z.array(z.string()).optional(),
|
||||
response_types_supported: z.array(z.string()),
|
||||
response_modes_supported: z.array(z.string()).optional(),
|
||||
grant_types_supported: z.array(z.string()).optional(),
|
||||
acr_values_supported: z.array(z.string()).optional(),
|
||||
subject_types_supported: z.array(z.string()),
|
||||
id_token_signing_alg_values_supported: z.array(z.string()),
|
||||
id_token_encryption_alg_values_supported: z.array(z.string()).optional(),
|
||||
id_token_encryption_enc_values_supported: z.array(z.string()).optional(),
|
||||
userinfo_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
userinfo_encryption_alg_values_supported: z.array(z.string()).optional(),
|
||||
userinfo_encryption_enc_values_supported: z.array(z.string()).optional(),
|
||||
request_object_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
request_object_encryption_alg_values_supported: z.array(z.string()).optional(),
|
||||
request_object_encryption_enc_values_supported: z.array(z.string()).optional(),
|
||||
token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
|
||||
token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
|
||||
display_values_supported: z.array(z.string()).optional(),
|
||||
claim_types_supported: z.array(z.string()).optional(),
|
||||
claims_supported: z.array(z.string()).optional(),
|
||||
service_documentation: z.string().optional(),
|
||||
claims_locales_supported: z.array(z.string()).optional(),
|
||||
ui_locales_supported: z.array(z.string()).optional(),
|
||||
claims_parameter_supported: z.boolean().optional(),
|
||||
request_parameter_supported: z.boolean().optional(),
|
||||
request_uri_parameter_supported: z.boolean().optional(),
|
||||
require_request_uri_registration: z.boolean().optional(),
|
||||
op_policy_uri: SafeUrlSchema.optional(),
|
||||
op_tos_uri: SafeUrlSchema.optional(),
|
||||
client_id_metadata_document_supported: z.boolean().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* OpenID Connect Discovery metadata that may include OAuth 2.0 fields
|
||||
* This schema represents the real-world scenario where OIDC providers
|
||||
* return a mix of OpenID Connect and OAuth 2.0 metadata fields
|
||||
*/
|
||||
export const OpenIdProviderDiscoveryMetadataSchema = OpenIdProviderMetadataSchema.merge(
|
||||
OAuthMetadataSchema.pick({
|
||||
export const OpenIdProviderDiscoveryMetadataSchema = z.object({
|
||||
...OpenIdProviderMetadataSchema.shape,
|
||||
...OAuthMetadataSchema.pick({
|
||||
code_challenge_methods_supported: true
|
||||
})
|
||||
);
|
||||
}).shape
|
||||
});
|
||||
|
||||
/**
|
||||
* OAuth 2.1 token response
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { describe, expect, test, beforeEach } from 'vitest';
|
|||
import { Protocol } from './protocol.js';
|
||||
import { Transport } from './transport.js';
|
||||
import { Request, Notification, Result, JSONRPCMessage } from '../types.js';
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
// Mock Transport class
|
||||
class MockTransport implements Transport {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ZodLiteral, ZodObject, ZodType, z } from 'zod';
|
||||
import { AnySchema, AnyObjectSchema, SchemaOutput, safeParse } from '../server/zod-compat.js';
|
||||
import {
|
||||
CancelledNotificationSchema,
|
||||
ClientCapabilities,
|
||||
|
|
@ -27,6 +27,7 @@ import {
|
|||
} from '../types.js';
|
||||
import { Transport, TransportSendOptions } from './transport.js';
|
||||
import { AuthInfo } from '../server/auth/types.js';
|
||||
import { getMethodLiteral, parseWithCompat } from '../server/zod-json-schema-compat.js';
|
||||
|
||||
/**
|
||||
* Callback for progress notifications.
|
||||
|
|
@ -152,7 +153,7 @@ export type RequestHandlerExtra<SendRequestT extends Request, SendNotificationT
|
|||
*
|
||||
* This is used by certain transports to correctly associate related messages.
|
||||
*/
|
||||
sendRequest: <U extends ZodType<object>>(request: SendRequestT, resultSchema: U, options?: RequestOptions) => Promise<z.infer<U>>;
|
||||
sendRequest: <U extends AnySchema>(request: SendRequestT, resultSchema: U, options?: RequestOptions) => Promise<SchemaOutput<U>>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -490,7 +491,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
*
|
||||
* Do not use this method to emit notifications! Use notification() instead.
|
||||
*/
|
||||
request<T extends ZodType<object>>(request: SendRequestT, resultSchema: T, options?: RequestOptions): Promise<z.infer<T>> {
|
||||
request<T extends AnySchema>(request: SendRequestT, resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>> {
|
||||
const { relatedRequestId, resumptionToken, onresumptiontoken } = options ?? {};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -555,8 +556,13 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
}
|
||||
|
||||
try {
|
||||
const result = resultSchema.parse(response.result);
|
||||
resolve(result);
|
||||
const parseResult = safeParse(resultSchema, response.result);
|
||||
if (!parseResult.success) {
|
||||
// Type guard: if success is false, error is guaranteed to exist
|
||||
reject(parseResult.error);
|
||||
} else {
|
||||
resolve(parseResult.data as SchemaOutput<T>);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
|
|
@ -639,19 +645,19 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
*
|
||||
* Note that this will replace any previous request handler for the same method.
|
||||
*/
|
||||
setRequestHandler<
|
||||
T extends ZodObject<{
|
||||
method: ZodLiteral<string>;
|
||||
}>
|
||||
>(
|
||||
setRequestHandler<T extends AnyObjectSchema>(
|
||||
requestSchema: T,
|
||||
handler: (request: z.infer<T>, extra: RequestHandlerExtra<SendRequestT, SendNotificationT>) => SendResultT | Promise<SendResultT>
|
||||
handler: (
|
||||
request: SchemaOutput<T>,
|
||||
extra: RequestHandlerExtra<SendRequestT, SendNotificationT>
|
||||
) => SendResultT | Promise<SendResultT>
|
||||
): void {
|
||||
const method = requestSchema.shape.method.value;
|
||||
const method = getMethodLiteral(requestSchema);
|
||||
this.assertRequestHandlerCapability(method);
|
||||
|
||||
this._requestHandlers.set(method, (request, extra) => {
|
||||
return Promise.resolve(handler(requestSchema.parse(request), extra));
|
||||
const parsed = parseWithCompat(requestSchema, request) as SchemaOutput<T>;
|
||||
return Promise.resolve(handler(parsed, extra));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -676,14 +682,15 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
*
|
||||
* Note that this will replace any previous notification handler for the same method.
|
||||
*/
|
||||
setNotificationHandler<
|
||||
T extends ZodObject<{
|
||||
method: ZodLiteral<string>;
|
||||
}>
|
||||
>(notificationSchema: T, handler: (notification: z.infer<T>) => void | Promise<void>): void {
|
||||
this._notificationHandlers.set(notificationSchema.shape.method.value, notification =>
|
||||
Promise.resolve(handler(notificationSchema.parse(notification)))
|
||||
);
|
||||
setNotificationHandler<T extends AnyObjectSchema>(
|
||||
notificationSchema: T,
|
||||
handler: (notification: SchemaOutput<T>) => void | Promise<void>
|
||||
): void {
|
||||
const method = getMethodLiteral(notificationSchema);
|
||||
this._notificationHandlers.set(method, notification => {
|
||||
const parsed = parseWithCompat(notificationSchema, notification) as SchemaOutput<T>;
|
||||
return Promise.resolve(handler(parsed));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue