1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-14 23:45:37 +00:00

feat: remove automatic value passing

This commit is contained in:
Andreas Coroiu
2025-10-31 09:40:03 +01:00
parent 60d0ac9bfc
commit cab3ae843b
6 changed files with 76 additions and 103 deletions

View File

@@ -34,6 +34,17 @@ export function chain<T extends object>(p: Promise<T>): ChainablePromise<T> {
get(_t, prop: string | symbol) {
return (...args: any[]) =>
Promise.resolve(p).then(async (obj) => {
// Special-case: allow uniform `.await.by_value()` usage on both references and plain values.
if (prop === "by_value") {
// If the object has a callable by_value, call it; otherwise return the object as-is.
const maybe = (obj as any)[prop];
if (typeof maybe === "function") {
const result = await maybe.apply(obj, args);
return wrapIfObject(result);
}
return obj;
}
const member = (obj as any)[prop];
if (typeof member === "function") {
const result = await member.apply(obj, args);

View File

@@ -5,6 +5,7 @@ export type Remote<T> = {
};
type Resolved<T> = T extends Promise<infer U> ? U : T;
type HasFree<T> = T extends { free(): void } ? true : false;
/**
* Maps remote object fields to RPC-exposed types.
@@ -21,104 +22,31 @@ type Resolved<T> = T extends Promise<infer U> ? U : T;
*/
export type RemoteProperty<T> = T extends (...args: any[]) => any
? RemoteFunction<T>
: IsSerializable<Resolved<T>> extends true
? Promise<Resolved<T>>
: Remote<Resolved<T>>;
: HasFree<Resolved<T>> extends true
? RemoteReference<Resolved<T>>
: Promise<Resolved<T>>;
export type RemoteReference<T> = Remote<T>;
export type RemoteReference<T> = Remote<T> & {
/**
* Force a by-value snapshot transfer of this remote reference. Resolves to a serializable value.
* If the object is not serializable at runtime, this will throw.
*/
by_value(): Promise<T>;
};
/**
* RemoteFunction arguments must be Serializable at compile time. For non-serializable
* return types, we expose ChainablePromise<Remote<...>> to enable Rust-like `.await` chaining.
*/
export type RemoteFunction<T extends (...args: any[]) => any> = <A extends Parameters<T>>(
// Enforce serializability of RPC arguments here.
// If we wanted to we could allow for remote references as arguments, we could do that here.
// In that case the client would also need to maintain a ReferenceStore for outgoing references.
...args: SerializableArgs<A>
) => IsSerializable<Resolved<ReturnType<T>>> extends true
? Promise<Resolved<ReturnType<T>>>
: ChainablePromise<Remote<Resolved<ReturnType<T>>>>;
export type RemoteFunction<T extends (...args: any[]) => any> = (
...args: Parameters<T>
) => Resolved<ReturnType<T>> extends object
? ChainablePromise<RemoteReference<Resolved<ReturnType<T>>>>
: Promise<Resolved<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>}]`>;
};
// Serializability checks removed: transport and server decide value vs reference.

View File

@@ -13,6 +13,7 @@ export type Command =
propertySymbol: SerializedPropertySymbol;
args: unknown[];
}
| { method: "by_value"; referenceId: ReferenceId }
| { method: "release"; referenceId: ReferenceId };
export type Response = { status: "success"; result: Result } | { status: "error"; error: unknown };

View File

@@ -11,8 +11,8 @@ export class RpcObjectReference {
channel: RpcRequestChannel,
referenceId: ReferenceId,
objectType?: string,
): RpcObjectReference {
return ProxiedReference(channel, new RpcObjectReference(referenceId, objectType));
): RpcObjectReference & { by_value(): Promise<any> } {
return ProxiedReference(channel, new RpcObjectReference(referenceId, objectType)) as any;
}
private constructor(
@@ -24,18 +24,33 @@ export class RpcObjectReference {
function ProxiedReference(
channel: RpcRequestChannel,
reference: RpcObjectReference,
): RpcObjectReference {
return new Proxy(reference, {
): RpcObjectReference & { by_value(): Promise<any> } {
return new Proxy(reference as any, {
get(target, property: string | PropertySymbol) {
if (property === "then") {
// Allow awaiting the proxy itself
return undefined;
}
if (property === "by_value") {
return async () => {
const result = await sendAndUnwrap(channel, {
method: "by_value",
referenceId: reference.referenceId,
} as Command);
if (result.type !== "value") {
throw new Error(
`[RPC] by_value() expected a value but got a reference for ${reference.objectType}`,
);
}
return result.value;
};
}
// console.log(`Accessing ${reference.objectType}.${String(propertyName)}`);
return RpcPropertyReference(channel, { objectReference: target, property });
return RpcPropertyReference(channel, { objectReference: target as any, property });
},
});
}) as any;
}
/**
@@ -88,7 +103,7 @@ function RpcPropertyReference(channel: RpcRequestChannel, reference: RpcProperty
// `Accessing ${reference.objectReference.objectType}.${reference.propertyName}.${propertyName}`,
// );
// Allow Function.prototype.call/apply/bind to be used by TS helpers and wrappers (e.g., disposables, chainable await)
// Allow Function.prototype.call/apply/bind to be used by TS helpers and wrappers (e.g., disposables, chainable await)
if (propertyName === "call") {
return Function.prototype.call;
}
@@ -130,7 +145,7 @@ function RpcPropertyReference(channel: RpcRequestChannel, reference: RpcProperty
};
}
return (onFulfilled: (value: any) => void, onRejected: (error: any) => void) => {
return (onFulfilled: (value: any) => void, onRejected: (error: any) => void) => {
(async () => {
const result = await sendAndUnwrap(channel, buildGetCommand(reference));
return unwrapResult(channel, result);
@@ -152,7 +167,7 @@ function RpcPropertyReference(channel: RpcRequestChannel, reference: RpcProperty
}
return RpcObjectReference.create(channel, result.referenceId, result.objectType);
})();
return chain(p as Promise<any>);
return chain(p as Promise<any>);
},
});
}

View File

@@ -36,6 +36,25 @@ export class RpcServer<T> {
}
}
if (command.method === "by_value") {
const target = this.references.get<any>(command.referenceId);
if (!target) {
return { status: "error", error: `[RPC] Reference ID ${command.referenceId} not found` };
}
try {
if (!isSerializable(target)) {
return {
status: "error",
error: `[RPC] by_value() not supported for non-serializable object of type ${target?.constructor?.name}`,
};
}
return { status: "success", result: { type: "value", value: target } };
} catch (error) {
return { status: "error", error };
}
}
if (command.method === "call") {
const target = this.references.get<any>(command.referenceId);
if (!target) {

View File

@@ -36,14 +36,13 @@ export class TotpService implements TotpServiceAbstraction {
return timer(0, 1000).pipe(
switchMap(() => {
if (this.remoteSdkService) {
console.log("[TOTP] Using remote SDK service to generate TOTP");
// Using remote SDK service to generate TOTP
return this.remoteSdkService.remoteClient$.pipe(
switchMap(async (sdk) => {
using ref = await sdk.take();
// TODO: Bug, for some reason .await is not available after totp()
// return ref.value.vault().await.totp().await.generate_totp(key);
using ref = await sdk!.take();
const totp = await ref.value.vault().await.totp();
return totp.generate_totp(key);
// Force by-value transfer for the TOTP response
return totp.generate_totp(key).await.by_value();
}),
shareReplay({ bufferSize: 1, refCount: true }),
);