-
-
Notifications
You must be signed in to change notification settings - Fork 69
feat(rpc): runtime type validation for rpc calls #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fbfc375
fed5b20
31d18dd
e015103
7ae96aa
13805f2
bf6906d
85690dd
cb9c320
f898497
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| export { builtinRpcSchemas } from '../../core/src/node/rpc' | ||
| export { createDevToolsContext } from './node/context' | ||
| export { DevTools } from './node/plugins' | ||
| export { createDevToolsMiddleware } from './node/server' | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -42,6 +42,13 @@ export const builtinRpcDeclarations = [ | |||||
|
|
||||||
| export type BuiltinServerFunctions = RpcDefinitionsToFunctions<typeof builtinRpcDeclarations> | ||||||
|
|
||||||
| export const builtinRpcSchemas = new Map( | ||||||
| builtinRpcDeclarations.map(d => [ | ||||||
| d.name, | ||||||
| { args: d.argsSchema, returns: d.returnSchema }, | ||||||
| ]), | ||||||
| ) | ||||||
|
|
||||||
| export type BuiltinServerFunctionsStatic = RpcDefinitionsToFunctions< | ||||||
| RpcDefinitionsFilter<typeof builtinRpcDeclarations, 'static'> | ||||||
| > | ||||||
|
|
@@ -51,7 +58,7 @@ export type BuiltinServerFunctionsDump = { | |||||
| } | ||||||
|
|
||||||
| declare module '@vitejs/devtools-kit' { | ||||||
| export interface DevToolsRpcServerFunctions extends BuiltinServerFunctions {} | ||||||
| export interface DevToolsRpcServerFunctions extends BuiltinServerFunctions { } | ||||||
|
||||||
| export interface DevToolsRpcServerFunctions extends BuiltinServerFunctions { } | |
| export interface DevToolsRpcServerFunctions extends BuiltinServerFunctions {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { UAParser } from 'my-ua-parser' | |
| import { createEventEmitter } from '../utils/events' | ||
| import { nanoid } from '../utils/nanoid' | ||
| import { promiseWithResolver } from '../utils/promise' | ||
| import { validateRpcArgs, validateRpcReturn } from '../utils/rpc-validation' | ||
| import { createRpcSharedStateClientHost } from './rpc-shared-state' | ||
|
|
||
| const CONNECTION_META_KEY = '__VITE_DEVTOOLS_CONNECTION_META__' | ||
|
|
@@ -241,10 +242,12 @@ export async function getDevToolsRpcClient( | |
| ensureTrusted, | ||
| requestTrust, | ||
| call: (...args: any): any => { | ||
| return serverRpc.$call( | ||
| validateRpcArgs(args[0], args.slice(1)) | ||
| const ret = serverRpc.$call( | ||
| // @ts-expect-error casting | ||
| ...args, | ||
| ) | ||
| validateRpcReturn(args[0], ret) | ||
| }, | ||
|
Comment on lines
244
to
251
|
||
| callEvent: (...args: any): any => { | ||
| return serverRpc.$callEvent( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,39 @@ | ||
| import type { RpcFunctionDefinition, RpcFunctionType } from 'birpc-x' | ||
| import type { GenericSchema } from 'valibot' | ||
| import type { DevToolsNodeContext } from '../types' | ||
| import { createDefineWrapperWithContext } from 'birpc-x' | ||
|
|
||
| export const defineRpcFunction = createDefineWrapperWithContext<DevToolsNodeContext>() | ||
| export interface RpcOptions< | ||
| NAME extends string, | ||
| TYPE extends RpcFunctionType, | ||
| A extends any[], | ||
| R, | ||
| AS extends GenericSchema[] | undefined = undefined, | ||
| RS extends GenericSchema | undefined = undefined, | ||
| > | ||
| extends RpcFunctionDefinition<NAME, TYPE, A, R, DevToolsNodeContext> { | ||
| args?: AS | ||
| return?: RS | ||
| } | ||
|
|
||
| export function defineRpcFunction< | ||
| NAME extends string, | ||
| TYPE extends RpcFunctionType, | ||
| A extends any[], | ||
| R, | ||
| AS extends GenericSchema[] | undefined = undefined, | ||
| RS extends GenericSchema | undefined = undefined, | ||
| >( | ||
| options: RpcOptions<NAME, TYPE, A, R, AS, RS>, | ||
| ) { | ||
| const { args, return: ret, ...rest } = options | ||
| const birpc = createDefineWrapperWithContext<DevToolsNodeContext>() | ||
|
|
||
| const fn = birpc(rest) | ||
|
|
||
| const augmentedFn = fn as typeof fn & { argsSchema?: AS, returnSchema?: RS } | ||
| augmentedFn.argsSchema = args | ||
| augmentedFn.returnSchema = ret | ||
|
|
||
| return augmentedFn | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import type { GenericSchema } from 'valibot' | ||
| import { builtinRpcSchemas } from '@vitejs/devtools' | ||
| import { serverRpcSchemas } from '@vitejs/devtools-vite' | ||
| import { parse } from 'valibot' | ||
|
|
||
| const rpcSchemas = new Map([ | ||
| ...builtinRpcSchemas, | ||
| ...serverRpcSchemas, | ||
| ]) | ||
|
|
||
| export function validateSchema(schema: GenericSchema, data: any, prefix: string) { | ||
| try { | ||
| parse(schema, data) | ||
| } | ||
| catch (e) { | ||
| throw new Error(`${prefix}: ${(e as Error).message}`) | ||
| } | ||
| } | ||
|
|
||
| function getSchema(method: string) { | ||
| const schema = rpcSchemas.get(method as any) | ||
| if (!schema) | ||
| throw new Error(`RPC method "${method}" is not defined.`) | ||
| return schema | ||
| } | ||
|
|
||
| export function validateRpcArgs(method: string, args: any[]) { | ||
| const schema = getSchema(method) | ||
| if (!schema) | ||
| throw new Error(`RPC method "${method}" is not defined.`) | ||
|
Comment on lines
+28
to
+30
|
||
|
|
||
| const { args: argsSchema } = schema | ||
| if (!argsSchema) | ||
| return | ||
|
|
||
| if (argsSchema.length !== args.length) | ||
| throw new Error(`Invalid number of arguments for RPC method "${method}". Expected ${argsSchema.length}, got ${args.length}.`) | ||
|
|
||
| for (let i = 0; i < argsSchema.length; i++) { | ||
| const s = argsSchema[i] | ||
| if (!s) | ||
| continue | ||
| validateSchema(s, args[i], `Invalid argument #${i + 1}`) | ||
| } | ||
| } | ||
|
|
||
| export function validateRpcReturn(method: string, data: any) { | ||
| const schema = getSchema(method) | ||
| if (!schema) | ||
| throw new Error(`RPC method "${method}" is not defined.`) | ||
|
Comment on lines
+47
to
+50
|
||
|
|
||
| const { returns: returnSchema } = schema | ||
| if (!returnSchema) | ||
| return | ||
|
|
||
| validateSchema(returnSchema, data, `Invalid return value for RPC method "${method}"`) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export * from './plugin' | ||
| export { serverRpcSchemas } from './rpc/index' |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| .: | ||
| DevToolsViteUI: function | ||
| serverRpcSchemas: object | ||
| ./dirs: | ||
| clientPublicDir: string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import path is incorrect. It should be './node/rpc' instead of '../../core/src/node/rpc'. The current path goes up two directories to packages/, then back down to core/src/node/rpc, which is unnecessarily complex and could cause issues if the directory structure changes.