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:
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user