From 77fbdb53d1860f1eb95f67c07b27cacec1020546 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 3 Nov 2025 11:11:38 +0100 Subject: [PATCH] feat: replace everything with new proxies --- .../popup/services/remote-sdk.service.ts | 10 +- .../src/platform/services/sdk/messages.ts | 29 +- .../services/sdk/remote-sdk-server.service.ts | 4 +- libs/common/src/enums/feature-flag.enum.ts | 6 +- .../services/sdk/remote-sdk.service.ts | 2 +- .../src/platform/services/sdk/remote.ts | 65 ---- .../services/sdk/rpc/batch-proxies.ts | 168 --------- .../sdk/{ => rpc}/chainable-promise.ts | 0 .../src/platform/services/sdk/rpc/client.ts | 5 +- ...atch-executor.spec.ts => executor.spec.ts} | 2 +- .../rpc/{batch-executor.ts => executor.ts} | 0 ...{batch-proxies.spec.ts => proxies.spec.ts} | 2 +- .../src/platform/services/sdk/rpc/proxies.ts | 328 ++++++++---------- .../src/platform/services/sdk/rpc/remote.ts | 32 ++ .../src/platform/services/sdk/rpc/rpc.spec.ts | 4 +- .../src/platform/services/sdk/rpc/server.ts | 66 +--- .../default-cipher-encryption.service.ts | 8 +- .../common/src/vault/services/totp.service.ts | 6 +- 18 files changed, 207 insertions(+), 530 deletions(-) delete mode 100644 libs/common/src/platform/services/sdk/remote.ts delete mode 100644 libs/common/src/platform/services/sdk/rpc/batch-proxies.ts rename libs/common/src/platform/services/sdk/{ => rpc}/chainable-promise.ts (100%) rename libs/common/src/platform/services/sdk/rpc/{batch-executor.spec.ts => executor.spec.ts} (99%) rename libs/common/src/platform/services/sdk/rpc/{batch-executor.ts => executor.ts} (100%) rename libs/common/src/platform/services/sdk/rpc/{batch-proxies.spec.ts => proxies.spec.ts} (99%) create mode 100644 libs/common/src/platform/services/sdk/rpc/remote.ts diff --git a/apps/browser/src/platform/popup/services/remote-sdk.service.ts b/apps/browser/src/platform/popup/services/remote-sdk.service.ts index b1162edd638..bc5b0a93342 100644 --- a/apps/browser/src/platform/popup/services/remote-sdk.service.ts +++ b/apps/browser/src/platform/popup/services/remote-sdk.service.ts @@ -35,12 +35,12 @@ export class BrowserRemoteSdkService implements RemoteSdkService { ); }, sendCommand: async (command) => { - this.logService.debug("[RemoteSdkService]: Sending command", command); - const response = (await BrowserApi.sendMessageWithResponse("sdk.request", { + const jsonResponse = (await BrowserApi.sendMessageWithResponse("sdk.request", { type: "RemoteSdkRequest", - command, - } satisfies RemoteSdkRequest)) as Response; - this.logService.debug("[RemoteSdkService]: Received response", response); + command: JSON.stringify(command), + } satisfies RemoteSdkRequest)) as string; + this.logService.debug("[RemoteSdkService]: Request:", command, "Response:", jsonResponse); + const response = JSON.parse(jsonResponse) as Response; return response; }, }); diff --git a/apps/browser/src/platform/services/sdk/messages.ts b/apps/browser/src/platform/services/sdk/messages.ts index c8d9ac7fa63..1dad4c2b890 100644 --- a/apps/browser/src/platform/services/sdk/messages.ts +++ b/apps/browser/src/platform/services/sdk/messages.ts @@ -1,13 +1,13 @@ -import { Command, Response, Result } from "@bitwarden/common/platform/services/sdk/rpc/protocol"; +import { Result } from "@bitwarden/common/platform/services/sdk/rpc/protocol"; export type RemoteSdkRequest = { type: "RemoteSdkRequest"; - command: Command; + command: string; }; export type RemoteSdkResponse = { type: "RemoteSdkResponse"; - response: Response; + response: string; }; export type RemoteSdkResendRootRequest = { @@ -21,37 +21,22 @@ export type RemoteSdkRoot = { export function isRemoteSdkRequest(message: unknown): message is RemoteSdkRequest { return ( - typeof message === "object" && - message !== null && - (message as any).type === "RemoteSdkRequest" && - typeof (message as any).command === "object" + typeof message === "object" && message !== null && (message as any).type === "RemoteSdkRequest" ); } export function isRemoteSdkResponse(message: unknown): message is RemoteSdkResponse { return ( - typeof message === "object" && - message !== null && - (message as any).type === "RemoteSdkResponse" && - typeof (message as any).response === "object" + typeof message === "object" && message !== null && (message as any).type === "RemoteSdkResponse" ); } export function isRemoteSdkResendRootRequest( message: unknown, ): message is RemoteSdkResendRootRequest { - return ( - typeof message === "object" && - message !== null && - (message as any).type === "RemoteSdkResendRootRequest" - ); + return message !== null && (message as any).type === "RemoteSdkResendRootRequest"; } export function isRemoteSdkRoot(message: unknown): message is RemoteSdkRoot { - return ( - typeof message === "object" && - message !== null && - (message as any).type === "RemoteSdkRoot" && - typeof (message as any).result === "object" - ); + return message !== null && (message as any).type === "RemoteSdkRoot"; } diff --git a/apps/browser/src/platform/services/sdk/remote-sdk-server.service.ts b/apps/browser/src/platform/services/sdk/remote-sdk-server.service.ts index 2e1962ba116..1fb80db5c5e 100644 --- a/apps/browser/src/platform/services/sdk/remote-sdk-server.service.ts +++ b/apps/browser/src/platform/services/sdk/remote-sdk-server.service.ts @@ -62,8 +62,8 @@ export class RemoteSdkServerService { "sdk.request", (message: any, sender: chrome.runtime.MessageSender, sendResponse: any) => { if (isRemoteSdkRequest(message)) { - void this.server.handle(message.command).then((response) => { - sendResponse(response); + void this.server.handle(JSON.parse(message.command)).then((response) => { + sendResponse(JSON.stringify(response)); }); return true; // Indicate that we will send a response asynchronously } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 3b94f09854a..4da86ecdd6b 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -97,9 +97,9 @@ export const DefaultFeatureFlagValue = { /* Vault */ [FeatureFlag.CipherKeyEncryption]: FALSE, - [FeatureFlag.PM19941MigrateCipherDomainToSdk]: true, - [FeatureFlag.PM22134SdkCipherListView]: true, - [FeatureFlag.PM22136_SdkCipherEncryption]: true, + [FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE, + [FeatureFlag.PM22134SdkCipherListView]: FALSE, + [FeatureFlag.PM22136_SdkCipherEncryption]: FALSE, /* Auth */ [FeatureFlag.PM22110_DisableAlternateLoginMethods]: FALSE, diff --git a/libs/common/src/platform/services/sdk/remote-sdk.service.ts b/libs/common/src/platform/services/sdk/remote-sdk.service.ts index b62c3834e66..130b1d64a7f 100644 --- a/libs/common/src/platform/services/sdk/remote-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/remote-sdk.service.ts @@ -4,7 +4,7 @@ import { BitwardenClient } from "@bitwarden/sdk-internal"; import { Rc } from "../../misc/reference-counting/rc"; -import { Remote } from "./remote"; +import { Remote } from "./rpc/remote"; export abstract class RemoteSdkService { abstract remoteClient$: Observable | null>>; diff --git a/libs/common/src/platform/services/sdk/remote.ts b/libs/common/src/platform/services/sdk/remote.ts deleted file mode 100644 index 5576f787dc0..00000000000 --- a/libs/common/src/platform/services/sdk/remote.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ChainablePromise } from "./chainable-promise"; - -export type Remote = { - [K in keyof T as K extends typeof Symbol.dispose ? never : K]: RemoteProperty; -} & (typeof Symbol.dispose extends keyof T ? { [Symbol.asyncDispose](): Promise } : object); - -type Resolved = T extends Promise ? U : T; -// type HasFree = T extends { free(): void } ? true : false; - -/** - * Maps remote object fields to RPC-exposed types. - * - * Property access (non-function): - * - If the value is serializable (see IsSerializable), returns Promise>. - * - If not serializable (e.g., class instance, Wasm object), returns Remote> (a live reference). - * Note: properties do NOT expose `.await`; they are direct remote references. - * - * Function call: - * - If the return value is serializable, returns Promise>. - * - If not serializable, returns ChainablePromise>> so callers can use `.await` - * for ergonomic chaining, e.g. remote.vault().await.totp().await.generate(...). - */ -export type RemoteProperty = T extends (...args: any[]) => any - ? RemoteFunction - : RemoteReference>; -// : HasFree> extends true -// ? RemoteReference> -// : Promise>; - -export type Transfer = { - /** - * 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. - */ - transfer: Promise; -}; - -export type RemoteReference = Remote & - Transfer & { - /** - * 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. - * - * OLD: Remove - */ - by_value(): Promise; - }; - -/** - * RemoteFunction arguments must be Serializable at compile time. For non-serializable - * return types, we expose ChainablePromise> to enable Rust-like `.await` chaining. - */ -export type RemoteFunction any> = ( - ...args: Parameters -) => ChainablePromise>>> & Transfer>>; -// ) => Resolved> extends object -// ? ChainablePromise>>> -// : Promise>>; - -// 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 -// Serializability checks removed: transport and server decide value vs reference. diff --git a/libs/common/src/platform/services/sdk/rpc/batch-proxies.ts b/libs/common/src/platform/services/sdk/rpc/batch-proxies.ts deleted file mode 100644 index b2e3ecbd773..00000000000 --- a/libs/common/src/platform/services/sdk/rpc/batch-proxies.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { RpcRequestChannel } from "./client"; -import { ReferenceId, PropertySymbol, BatchCommand, serializeSymbol } from "./protocol"; - -export type BatchingProxy = { - [ProxyInfo]: T & { - proxyType: "RpcObjectReference" | "RpcPendingObjectReference"; - }; -}; - -export const ProxyInfo = Symbol("ProxyInfo"); - -export function isProxy(obj: any): obj is BatchingProxy { - return obj && typeof obj === "function" && obj[ProxyInfo] !== undefined; -} - -export function isReferenceProxy(obj: any): obj is BatchingProxy { - return isProxy(obj) && obj[ProxyInfo].proxyType === "RpcObjectReference"; -} - -export function isPendingReferenceProxy(obj: any): obj is BatchingProxy { - return isProxy(obj) && obj[ProxyInfo].proxyType === "RpcPendingObjectReference"; -} - -/** - * A reference to a remote object. - */ -export type RpcObjectReference = { - referenceId: ReferenceId; - objectType?: string; -}; - -export function RpcObjectReference(channel: RpcRequestChannel, reference: RpcObjectReference) { - const target = () => {}; - Object.defineProperty(target, "name", { value: `RpcObjectReference`, configurable: true }); - (target as any)[ProxyInfo] = { ...reference, proxyType: "RpcObjectReference" }; - - return new Proxy( - target, - proxyHandler(target, channel, reference, []), - ) as any as BatchingProxy; -} - -/** - * A pending reference to a remote object. - */ -export type RpcPendingObjectReference = { - reference: RpcObjectReference; - commands: BatchCommand[]; -}; - -export function RpcPendingObjectReference( - channel: RpcRequestChannel, - reference: RpcPendingObjectReference, -) { - const target = () => {}; - Object.defineProperty(target, "name", { - value: `RpcPendingObjectReference(${reference.reference.objectType}.${commandsToString(reference.commands)})`, - configurable: true, - }); - (target as any)[ProxyInfo] = { ...reference, proxyType: "RpcPendingObjectReference" }; - - return new Proxy( - target, - proxyHandler(target, channel, reference.reference, reference.commands), - ) as any as BatchingProxy; -} - -function proxyHandler( - target: any, - channel: RpcRequestChannel, - reference: RpcObjectReference, - commands: BatchCommand[], -): any { - return { - get(target: any, property: string | PropertySymbol) { - if ((property as any) === ProxyInfo) { - return (target as any)[ProxyInfo]; - } - - if (property === "then" && commands.length === 0) { - // This means we awaited a RpcObjectReference which resolves to itself - // We don't support transfering references to Promises themselves, we'll - // automatically await them before returning - return undefined; - } - - if (property === "then" && commands.length > 0) { - return BatchCommandExecutor(channel, reference.referenceId, commands); - } - - if (property === "await") { - return RpcPendingObjectReference(channel, { - reference, - commands: [...commands, { method: "await" }], - }); - } - - if (property === "transfer") { - return RpcPendingObjectReference(channel, { - reference, - commands: [...commands, { method: "transfer" }], - }); - } - - return RpcPendingObjectReference(channel, { - reference, - commands: - typeof property === "string" - ? [...commands, { method: "get", propertyName: property }] - : [...commands, { method: "get", propertySymbol: serializeSymbol(property) }], - }); - }, - - apply(_target: any, _thisArg: any, argArray?: any): any { - return RpcPendingObjectReference(channel, { - reference, - commands: [...commands, { method: "apply", args: argArray }], - }); - }, - } satisfies ProxyHandler; -} - -function BatchCommandExecutor( - channel: RpcRequestChannel, - referenceId: ReferenceId, - commands: BatchCommand[], -): (onFulfilled: (value: any) => void, onRejected: (reason: any) => void) => void { - const command = { - method: "batch", - referenceId, - commands: commands.filter((cmd) => cmd.method !== "await"), - } as const; - - return (onFulfilled, onRejected) => { - (async () => { - const result = await channel.sendCommand(command); - - if (result.status === "error") { - throw result.error; - } - - if (result.result.type === "value") { - return result.result.value; - } - - return RpcObjectReference(channel, { - referenceId: result.result.referenceId, - objectType: result.result.objectType, - }); - })().then(onFulfilled, onRejected); - }; -} - -function commandsToString(commands: BatchCommand[]): string { - return commands - .map((cmd) => { - if (cmd.method === "get") { - const prop = (cmd as any).propertyName ?? (cmd as any).propertySymbol; - return `${String(prop)}`; - } else if (cmd.method === "apply") { - const prop = (cmd as any).propertyName ?? (cmd as any).propertySymbol; - return `${String(prop)}()`; - } - - return "???"; - }) - .join("."); -} diff --git a/libs/common/src/platform/services/sdk/chainable-promise.ts b/libs/common/src/platform/services/sdk/rpc/chainable-promise.ts similarity index 100% rename from libs/common/src/platform/services/sdk/chainable-promise.ts rename to libs/common/src/platform/services/sdk/rpc/chainable-promise.ts diff --git a/libs/common/src/platform/services/sdk/rpc/client.ts b/libs/common/src/platform/services/sdk/rpc/client.ts index 0e0967861fd..1e00e029f43 100644 --- a/libs/common/src/platform/services/sdk/rpc/client.ts +++ b/libs/common/src/platform/services/sdk/rpc/client.ts @@ -1,9 +1,8 @@ import { map, Observable } from "rxjs"; -import { Remote } from "../remote"; - -import { RpcObjectReference } from "./batch-proxies"; import { Command, Response } from "./protocol"; +import { RpcObjectReference } from "./proxies"; +import { Remote } from "./remote"; export interface RpcRequestChannel { sendCommand(command: Command): Promise; diff --git a/libs/common/src/platform/services/sdk/rpc/batch-executor.spec.ts b/libs/common/src/platform/services/sdk/rpc/executor.spec.ts similarity index 99% rename from libs/common/src/platform/services/sdk/rpc/batch-executor.spec.ts rename to libs/common/src/platform/services/sdk/rpc/executor.spec.ts index c051b409ab9..7ab9ef01c9b 100644 --- a/libs/common/src/platform/services/sdk/rpc/batch-executor.spec.ts +++ b/libs/common/src/platform/services/sdk/rpc/executor.spec.ts @@ -1,4 +1,4 @@ -import { executeBatchCommands } from "./batch-executor"; +import { executeBatchCommands } from "./executor"; import { RpcError } from "./error"; import { BatchCommand, serializeSymbol } from "./protocol"; import { ReferenceStore } from "./reference-store"; diff --git a/libs/common/src/platform/services/sdk/rpc/batch-executor.ts b/libs/common/src/platform/services/sdk/rpc/executor.ts similarity index 100% rename from libs/common/src/platform/services/sdk/rpc/batch-executor.ts rename to libs/common/src/platform/services/sdk/rpc/executor.ts diff --git a/libs/common/src/platform/services/sdk/rpc/batch-proxies.spec.ts b/libs/common/src/platform/services/sdk/rpc/proxies.spec.ts similarity index 99% rename from libs/common/src/platform/services/sdk/rpc/batch-proxies.spec.ts rename to libs/common/src/platform/services/sdk/rpc/proxies.spec.ts index bf0cd306285..8f42ad2cc92 100644 --- a/libs/common/src/platform/services/sdk/rpc/batch-proxies.spec.ts +++ b/libs/common/src/platform/services/sdk/rpc/proxies.spec.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { ProxyInfo, RpcObjectReference } from "./batch-proxies"; +import { ProxyInfo, RpcObjectReference } from "./proxies"; import { RpcRequestChannel } from "./client"; import { Command, Response } from "./protocol"; diff --git a/libs/common/src/platform/services/sdk/rpc/proxies.ts b/libs/common/src/platform/services/sdk/rpc/proxies.ts index f5608ec88ac..9d4f411db96 100644 --- a/libs/common/src/platform/services/sdk/rpc/proxies.ts +++ b/libs/common/src/platform/services/sdk/rpc/proxies.ts @@ -1,215 +1,173 @@ -import { chain } from "../chainable-promise"; - import { RpcRequestChannel } from "./client"; -import { Command, PropertySymbol, ReferenceId, Response, serializeSymbol } from "./protocol"; +import { RpcError } from "./error"; +import { ReferenceId, PropertySymbol, BatchCommand, serializeSymbol } from "./protocol"; + +export type BatchingProxy = { + [ProxyInfo]: T & { + proxyType: "RpcObjectReference" | "RpcPendingObjectReference"; + }; +}; + +export const ProxyInfo = Symbol("ProxyInfo"); + +export function isProxy(obj: any): obj is BatchingProxy { + return obj && typeof obj === "function" && obj[ProxyInfo] !== undefined; +} + +export function isReferenceProxy(obj: any): obj is BatchingProxy { + return isProxy(obj) && obj[ProxyInfo].proxyType === "RpcObjectReference"; +} + +export function isPendingReferenceProxy(obj: any): obj is BatchingProxy { + return isProxy(obj) && obj[ProxyInfo].proxyType === "RpcPendingObjectReference"; +} /** * A reference to a remote object. */ -export class RpcObjectReference { - static create( - channel: RpcRequestChannel, - referenceId: ReferenceId, - objectType?: string, - ): RpcObjectReference & { by_value(): Promise } { - return ProxiedReference(channel, new RpcObjectReference(referenceId, objectType)) as any; - } +export type RpcObjectReference = { + referenceId: ReferenceId; + objectType?: string; +}; - private constructor( - public referenceId: ReferenceId, - public objectType?: string, - ) {} +export function RpcObjectReference(channel: RpcRequestChannel, reference: RpcObjectReference) { + const target = () => {}; + Object.defineProperty(target, "name", { value: `RpcObjectReference`, configurable: true }); + (target as any)[ProxyInfo] = { ...reference, proxyType: "RpcObjectReference" }; + + return new Proxy( + target, + proxyHandler(target, channel, reference, []), + ) as any as BatchingProxy; } -function ProxiedReference( +/** + * A pending reference to a remote object. + */ +export type RpcPendingObjectReference = { + reference: RpcObjectReference; + commands: BatchCommand[]; +}; + +export function RpcPendingObjectReference( + channel: RpcRequestChannel, + reference: RpcPendingObjectReference, +) { + const target = () => {}; + Object.defineProperty(target, "name", { + value: `RpcPendingObjectReference(${reference.reference.objectType}.${commandsToString(reference.commands)})`, + configurable: true, + }); + (target as any)[ProxyInfo] = { ...reference, proxyType: "RpcPendingObjectReference" }; + + return new Proxy( + target, + proxyHandler(target, channel, reference.reference, reference.commands), + ) as any as BatchingProxy; +} + +function proxyHandler( + target: any, channel: RpcRequestChannel, reference: RpcObjectReference, -): RpcObjectReference & { by_value(): Promise } { - return new Proxy(reference as any, { - get(target, property: string | PropertySymbol) { - if (property === "then") { - // Allow awaiting the proxy itself + commands: BatchCommand[], +): any { + return { + get(target: any, property: string | PropertySymbol) { + if ((property as any) === ProxyInfo) { + return (target as any)[ProxyInfo]; + } + + if (property === "then" && commands.length === 0) { + // This means we awaited a RpcObjectReference which resolves to itself + // We don't support transfering references to Promises themselves, we'll + // automatically await them before returning 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; - }; + if (property === "then" && commands.length > 0) { + return BatchCommandExecutor(channel, reference.referenceId, commands); } - // console.log(`Accessing ${reference.objectType}.${String(propertyName)}`); - return RpcPropertyReference(channel, { objectReference: target as any, property }); + if (property === "await") { + return RpcPendingObjectReference(channel, { + reference, + commands: [...commands, { method: "await" }], + }); + } + + if (property === "transfer") { + return RpcPendingObjectReference(channel, { + reference, + commands: [...commands, { method: "transfer" }], + }); + } + + return RpcPendingObjectReference(channel, { + reference, + commands: + typeof property === "string" + ? [...commands, { method: "get", propertyName: property }] + : [...commands, { method: "get", propertySymbol: serializeSymbol(property) }], + }); }, - }) as any; + + apply(_target: any, _thisArg: any, argArray?: any): any { + return RpcPendingObjectReference(channel, { + reference, + commands: [...commands, { method: "apply", args: argArray }], + }); + }, + } satisfies ProxyHandler; } -/** - * A reference to a specific property on a remote object. - */ -type RpcPropertyReference = { - objectReference: RpcObjectReference; - property: string | PropertySymbol; -}; +function BatchCommandExecutor( + channel: RpcRequestChannel, + referenceId: ReferenceId, + commands: BatchCommand[], +): (onFulfilled: (value: any) => void, onRejected: (reason: any) => void) => void { + const command = { + method: "batch", + referenceId, + commands: commands.filter((cmd) => cmd.method !== "await"), + } as const; -/** - * A reference to a specific property on a remote object. - */ -// export class RpcPropertyReference { -// static create( -// channel: RpcRequestChannel, -// objectReference: RpcObjectReference, -// propertyName: string, -// ): RpcPropertyReference { -// return ProxiedReferenceProperty( -// channel, -// new RpcPropertyReference(objectReference, propertyName), -// ); -// } + return (onFulfilled, onRejected) => { + (async () => { + const result = await channel.sendCommand(command); -// private constructor( -// public objectReference: RpcObjectReference, -// public propertyName: string, -// ) {} -// } - -/** - * A sub-proxy for a specific property of a proxied reference - * This is because we need to handle property accesses differently than method calls - * but we don't know which type it is until it gets consumed. - * - * If this references a method then the `apply` trap will be called on this proxy. - * If this references a property then they'll try to await the value, triggering the `get` trap - * when they access the `then` property. - */ -function RpcPropertyReference(channel: RpcRequestChannel, reference: RpcPropertyReference) { - const target = () => {}; - Object.defineProperty(target, "name", { value: `RpcPropertyReference`, configurable: true }); - (target as any).objectReference = reference.objectReference; - (target as any).property = reference.property; - - return new Proxy(target, { - get(_target, propertyName: string) { - // console.log( - // `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) - if (propertyName === "call") { - return Function.prototype.call; - } - if (propertyName === "apply") { - return Function.prototype.apply; - } - if (propertyName === "bind") { - return Function.prototype.bind; + if (result === null || result === undefined) { + throw new RpcError("RPC returned null or undefined response"); } - if (propertyName !== "then") { - // Support chained call like: (await obj.prop).method() AND obj.prop.method() - // by lazily resolving obj.prop first, then invoking method/property on the resolved reference. - return (...argArray: unknown[]) => { - const p = (async () => { - // First resolve the original referenced property value via GET - const getResult = await sendAndUnwrap(channel, buildGetCommand(reference)); - if (getResult.type !== "reference") { - throw new Error( - `Cannot access property '${propertyName}' on non-reference value returned by remote property`, - ); - } - - // Now perform the requested operation on the resolved reference - const callResult = await sendAndUnwrap( - channel, - buildCallCommand(getResult.referenceId, propertyName, argArray), - ); - if (callResult.type === "value") { - return callResult.value; - } - return RpcObjectReference.create( - channel, - callResult.referenceId, - callResult.objectType, - ); - })(); - return chain(p as Promise); - }; + if (result.status === "error") { + throw result.error; } - return (onFulfilled: (value: any) => void, onRejected: (error: any) => void) => { - (async () => { - const result = await sendAndUnwrap(channel, buildGetCommand(reference)); - return unwrapResult(channel, result); - })().then(onFulfilled, onRejected); - }; - }, - apply(_target, _thisArg, argArray: unknown[]) { - // console.log(`Calling ${reference.objectReference.objectType}.${reference.propertyName}`); + if (result.result.type === "value") { + return result.result.value; + } - const command = buildCallCommand( - reference.objectReference.referenceId, - reference.property, - argArray, - ); - const p = (async () => { - const result = await sendAndUnwrap(channel, command); - if (result.type === "value") { - return result.value; - } - return RpcObjectReference.create(channel, result.referenceId, result.objectType); - })(); - return chain(p as Promise); - }, - }); -} - -// Helpers -function buildGetCommand(reference: RpcPropertyReference): Command { - if (typeof reference.property === "string") { - return { - method: "get", - referenceId: reference.objectReference.referenceId, - propertyName: reference.property, - }; - } - return { - method: "get", - referenceId: reference.objectReference.referenceId, - propertySymbol: serializeSymbol(reference.property), + return RpcObjectReference(channel, { + referenceId: result.result.referenceId, + objectType: result.result.objectType, + }); + })().then(onFulfilled, onRejected); }; } -function buildCallCommand( - referenceId: ReferenceId, - property: string | PropertySymbol, - args: unknown[], -): Command { - if (typeof property === "string") { - return { method: "call", referenceId, propertyName: property, args }; - } - return { method: "call", referenceId, propertySymbol: serializeSymbol(property), args }; -} +function commandsToString(commands: BatchCommand[]): string { + return commands + .map((cmd) => { + if (cmd.method === "get") { + const prop = (cmd as any).propertyName ?? (cmd as any).propertySymbol; + return `${String(prop)}`; + } else if (cmd.method === "apply") { + const prop = (cmd as any).propertyName ?? (cmd as any).propertySymbol; + return `${String(prop)}()`; + } -async function sendAndUnwrap(channel: RpcRequestChannel, command: Command) { - const response: Response = await channel.sendCommand(command); - if (response.status === "error") { - throw new Error(`RPC Error: ${response.error}`); - } - return response.result; -} - -function unwrapResult(channel: RpcRequestChannel, result: any) { - if (result.type === "value") { - return result.value; - } - return RpcObjectReference.create(channel, result.referenceId, result.objectType); + return "???"; + }) + .join("."); } diff --git a/libs/common/src/platform/services/sdk/rpc/remote.ts b/libs/common/src/platform/services/sdk/rpc/remote.ts new file mode 100644 index 00000000000..dea5e643e32 --- /dev/null +++ b/libs/common/src/platform/services/sdk/rpc/remote.ts @@ -0,0 +1,32 @@ +import { ChainablePromise } from "./chainable-promise"; + +export type Remote = { + [K in keyof T as K extends typeof Symbol.dispose ? never : K]: RemoteProperty; +} & (typeof Symbol.dispose extends keyof T ? { [Symbol.asyncDispose](): Promise } : object); + +type Resolved = T extends Promise ? U : T; + +/** + * Maps remote object fields to RPC-exposed types. + */ +export type RemoteProperty = T extends (...args: any[]) => any + ? RemoteFunction + : RemoteReference>; + +export type Transfer = { + /** + * 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. + */ + transfer: Promise; +}; + +export type RemoteReference = Remote & Transfer; + +/** + * RemoteFunction arguments must be Serializable at compile time. For non-serializable + * return types, we expose ChainablePromise> to enable Rust-like `.await` chaining. + */ +export type RemoteFunction any> = ( + ...args: Parameters +) => ChainablePromise>>> & Transfer>>; diff --git a/libs/common/src/platform/services/sdk/rpc/rpc.spec.ts b/libs/common/src/platform/services/sdk/rpc/rpc.spec.ts index 2461916113c..ced6e81a5df 100644 --- a/libs/common/src/platform/services/sdk/rpc/rpc.spec.ts +++ b/libs/common/src/platform/services/sdk/rpc/rpc.spec.ts @@ -2,9 +2,9 @@ import { firstValueFrom, map, Observable } from "rxjs"; import { Rc } from "../../../misc/reference-counting/rc"; -import { isReferenceProxy, RpcObjectReference } from "./batch-proxies"; import { RpcClient, RpcRequestChannel } from "./client"; import { Command, Response } from "./protocol"; +import { isReferenceProxy, RpcObjectReference } from "./proxies"; import { RpcServer } from "./server"; describe("RpcServer", () => { @@ -66,7 +66,7 @@ describe("RpcServer", () => { expect(result).toEqual({ message: "Hello, World!", array: [1, "2", null] }); }); - it("handles RC objects correctly", async () => { + it.only("handles RC objects correctly", async () => { const wasmObj = new WasmLikeObject("RC"); const rcValue = new Rc(wasmObj); const server = new RpcServer>(); diff --git a/libs/common/src/platform/services/sdk/rpc/server.ts b/libs/common/src/platform/services/sdk/rpc/server.ts index 8dff4a9ac5d..0a2b082af9a 100644 --- a/libs/common/src/platform/services/sdk/rpc/server.ts +++ b/libs/common/src/platform/services/sdk/rpc/server.ts @@ -1,6 +1,6 @@ import { map, Observable, ReplaySubject } from "rxjs"; -import { executeBatchCommands } from "./batch-executor"; +import { executeBatchCommands } from "./executor"; import { Command, Response, Result } from "./protocol"; import { ReferenceStore } from "./reference-store"; @@ -30,70 +30,6 @@ export class RpcServer { } } - // if (command.method === "get") { - // const target = this.references.get(command.referenceId); - // if (!target) { - // return { status: "error", error: `[RPC] Reference ID ${command.referenceId} not found` }; - // } - - // try { - // const propertyKey = - // (command as any).propertyName ?? deserializeSymbol((command as any).propertySymbol); - // const propertyValue = target[propertyKey]; - // if (typeof propertyValue === "function") { - // return { status: "error", error: `[RPC] Property ${String(propertyKey)} is a function` }; - // } else { - // return { status: "success", result: this.convertToReturnable(propertyValue) }; - // } - // } catch (error) { - // return { status: "error", error }; - // } - // } - - // if (command.method === "by_value") { - // const target = this.references.get(command.referenceId); - // if (!target) { - // return { status: "error", error: `[RPC] Reference ID ${command.referenceId} not found` }; - // } - - // // try { - // // Not a dependable check - // // 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(command.referenceId); - // if (!target) { - // return { status: "error", error: `[RPC] Reference ID ${command.referenceId} not found` }; - // } - - // try { - // const propertyKey = - // (command as any).propertyName ?? deserializeSymbol((command as any).propertySymbol); - // const method = target[propertyKey]; - // if (typeof method !== "function") { - // return { - // status: "error", - // error: `[RPC] Property ${String(propertyKey)} is not a function of ${target.constructor.name}`, - // }; - // } - - // const result = await method.apply(target, command.args); - // return { status: "success", result: this.convertToReturnable(result) }; - // } catch (error) { - // return { status: "error", error }; - // } - // } - return { status: "error", error: `Unknown command method: ${command.method}` }; } diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts index 6aacab7eb01..582210261a0 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts @@ -233,12 +233,12 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { throw new Error("SDK is undefined"); } - using ref = await sdk.take(); + await using ref = await sdk.take(); const ciphersClient = await ref.value.vault().await.ciphers(); - const result: DecryptCipherListResult = await ciphersClient - .decrypt_list_with_failures(ciphers.map((cipher) => cipher.toSdkCipher())) - .await.by_value(); + const result: DecryptCipherListResult = await ciphersClient.decrypt_list_with_failures( + ciphers.map((cipher) => cipher.toSdkCipher()), + ).transfer; const decryptedCiphers = result.successes; const failedCiphers: Cipher[] = result.failures diff --git a/libs/common/src/vault/services/totp.service.ts b/libs/common/src/vault/services/totp.service.ts index 04aac075e48..ca3e82ed8a6 100644 --- a/libs/common/src/vault/services/totp.service.ts +++ b/libs/common/src/vault/services/totp.service.ts @@ -39,10 +39,10 @@ export class TotpService implements TotpServiceAbstraction { // Using remote SDK service to generate TOTP return this.remoteSdkService.remoteClient$.pipe( switchMap(async (sdk) => { - using ref = await sdk!.take(); + await using ref = await sdk!.take(); const totp = await ref.value.vault().await.totp(); - // Force by-value transfer for the TOTP response - return totp.generate_totp(key).await.by_value(); + // Transfer the TOTP response + return totp.generate_totp(key).transfer; }), shareReplay({ bufferSize: 1, refCount: true }), );