mirror of
https://github.com/bitwarden/browser
synced 2026-02-05 03:03:26 +00:00
feat: ban unserializable args
This commit is contained in:
@@ -16,6 +16,89 @@ export type RemoteValue<T> = T extends { free(): void }
|
||||
? Promise<R>
|
||||
: Promise<T>;
|
||||
|
||||
export type RemoteFunction<T extends (...args: any[]) => any> = (
|
||||
...args: Parameters<T>
|
||||
export type RemoteFunction<T extends (...args: any[]) => any> = <A extends Parameters<T>>(
|
||||
...args: SerializableArgs<A>
|
||||
) => RemoteValue<ReturnType<T>>;
|
||||
|
||||
// Serializable type rules to mirror `isSerializable` from rpc/server.ts
|
||||
// - Primitives: string | number | boolean | null
|
||||
// - Arrays: elements must be Serializable
|
||||
// - Plain objects: all non-function properties must be Serializable
|
||||
// - Everything else (functions, class instances, Date, Map, Set, etc.) is NOT serializable
|
||||
type IsAny<T> = 0 extends 1 & T ? true : false;
|
||||
type IsNever<T> = [T] extends [never] ? true : false;
|
||||
|
||||
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
|
||||
type IsArray<T> = T extends readonly any[] ? true : false;
|
||||
|
||||
type IsSerializablePrimitive<T> = [T] extends [string | number | boolean | null] ? true : false;
|
||||
|
||||
type IsSerializableArray<T> = T extends readonly (infer U)[] ? IsSerializable<U> : false;
|
||||
|
||||
type PropsAreSerializable<T> =
|
||||
Exclude<
|
||||
{
|
||||
[K in keyof T]-?: IsFunction<T[K]> extends true ? false : IsSerializable<T[K]>;
|
||||
}[keyof T],
|
||||
true
|
||||
> extends never
|
||||
? true
|
||||
: false;
|
||||
|
||||
type IsSpecialObject<T> = T extends
|
||||
| Date
|
||||
| RegExp
|
||||
| Map<any, any>
|
||||
| Set<any>
|
||||
| WeakMap<any, any>
|
||||
| WeakSet<any>
|
||||
? true
|
||||
: false;
|
||||
|
||||
type IsSerializableObject<T> =
|
||||
IsFunction<T> extends true
|
||||
? false
|
||||
: IsArray<T> extends true
|
||||
? false
|
||||
: IsSpecialObject<T> extends true
|
||||
? false
|
||||
: T extends object
|
||||
? PropsAreSerializable<T>
|
||||
: false;
|
||||
|
||||
export type IsSerializable<T> =
|
||||
IsAny<T> extends true
|
||||
? false // discourage any; use explicit types
|
||||
: IsNever<T> extends true
|
||||
? true
|
||||
: IsSerializablePrimitive<T> extends true
|
||||
? true
|
||||
: IsArray<T> extends true
|
||||
? IsSerializableArray<T>
|
||||
: IsSerializableObject<T>;
|
||||
|
||||
// Public helper alias for consumers
|
||||
export type Serializable<T> = IsSerializable<T> extends true ? T : never;
|
||||
|
||||
// Human-readable reason per kind
|
||||
type NonSerializableReason<T> =
|
||||
IsFunction<T> extends true
|
||||
? "functions are not serializable"
|
||||
: IsArray<T> extends true
|
||||
? "array contains non-serializable element(s)"
|
||||
: IsSpecialObject<T> extends true
|
||||
? "class instances / special objects (Date/Map/Set/RegExp/...) are not serializable"
|
||||
: T extends object
|
||||
? "object contains non-serializable property"
|
||||
: "type is not serializable";
|
||||
|
||||
// Tuple-literal error type so TS prints a helpful message at the callsite
|
||||
type EnsureSerializableWithMessage<T, C extends string> =
|
||||
IsSerializable<T> extends true
|
||||
? T
|
||||
: ["Non-serializable RPC argument", C, NonSerializableReason<T>];
|
||||
|
||||
type IndexLabel<K> = K extends string | number ? `${K}` : "?";
|
||||
type SerializableArgs<A extends any[]> = {
|
||||
[K in keyof A]: EnsureSerializableWithMessage<A[K], `arg[${IndexLabel<K>}]`>;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { RpcClient, RpcRequestChannel } from "./client";
|
||||
import { Command, Response } from "./protocol";
|
||||
import { RpcObjectReference } from "./proxies";
|
||||
import { RpcServer } from "./server";
|
||||
|
||||
describe("RpcServer", () => {
|
||||
@@ -45,8 +46,21 @@ describe("RpcServer", () => {
|
||||
const wasmObj = await remoteInstance.getWasmGreeting("Wasm World");
|
||||
const greeting = await wasmObj.greet();
|
||||
|
||||
expect(wasmObj).toBeInstanceOf(RpcObjectReference);
|
||||
expect(greeting).toBe("Hello, Wasm World!");
|
||||
});
|
||||
|
||||
it("returns plain objects by value", async () => {
|
||||
const remoteInstance = await firstValueFrom(client.getRoot());
|
||||
|
||||
const result = await remoteInstance.echo({
|
||||
message: "Hello, World!",
|
||||
array: [1, "2", null],
|
||||
});
|
||||
|
||||
expect(result).not.toBeInstanceOf(RpcObjectReference);
|
||||
expect(result).toEqual({ message: "Hello, World!", array: [1, "2", null] });
|
||||
});
|
||||
});
|
||||
|
||||
class TestClass {
|
||||
@@ -63,6 +77,10 @@ class TestClass {
|
||||
getWasmGreeting(name: string): WasmLikeObject {
|
||||
return new WasmLikeObject(name);
|
||||
}
|
||||
|
||||
echo<T>(obj: T): T {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
class WasmLikeObject {
|
||||
|
||||
Reference in New Issue
Block a user