From cab3ae843b0257774bab22f1b8099d71de47a770 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Fri, 31 Oct 2025 09:40:03 +0100 Subject: [PATCH] feat: remove automatic value passing --- .../services/sdk/chainable-promise.ts | 11 ++ .../src/platform/services/sdk/remote.ts | 106 +++--------------- .../src/platform/services/sdk/rpc/protocol.ts | 1 + .../src/platform/services/sdk/rpc/proxies.ts | 33 ++++-- .../src/platform/services/sdk/rpc/server.ts | 19 ++++ .../common/src/vault/services/totp.service.ts | 9 +- 6 files changed, 76 insertions(+), 103 deletions(-) diff --git a/libs/common/src/platform/services/sdk/chainable-promise.ts b/libs/common/src/platform/services/sdk/chainable-promise.ts index 184b779b126..0b8af2e0a10 100644 --- a/libs/common/src/platform/services/sdk/chainable-promise.ts +++ b/libs/common/src/platform/services/sdk/chainable-promise.ts @@ -34,6 +34,17 @@ export function chain(p: Promise): ChainablePromise { 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); diff --git a/libs/common/src/platform/services/sdk/remote.ts b/libs/common/src/platform/services/sdk/remote.ts index 024a8b6e690..36b43061824 100644 --- a/libs/common/src/platform/services/sdk/remote.ts +++ b/libs/common/src/platform/services/sdk/remote.ts @@ -5,6 +5,7 @@ export type Remote = { }; type Resolved = T extends Promise ? U : T; +type HasFree = T extends { free(): void } ? true : false; /** * Maps remote object fields to RPC-exposed types. @@ -21,104 +22,31 @@ type Resolved = T extends Promise ? U : T; */ export type RemoteProperty = T extends (...args: any[]) => any ? RemoteFunction - : IsSerializable> extends true - ? Promise> - : Remote>; + : HasFree> extends true + ? RemoteReference> + : Promise>; -export type RemoteReference = Remote; +export type RemoteReference = Remote & { + /** + * 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; +}; /** * 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> = >( - // 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 -) => IsSerializable>> extends true - ? Promise>> - : ChainablePromise>>>; +export type RemoteFunction any> = ( + ...args: Parameters +) => 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 -type IsAny = 0 extends 1 & T ? true : false; -type IsNever = [T] extends [never] ? true : false; - -type IsFunction = T extends (...args: any[]) => any ? true : false; -type IsArray = T extends readonly any[] ? true : false; - -type IsSerializablePrimitive = [T] extends [string | number | boolean | null] ? true : false; - -type IsSerializableArray = T extends readonly (infer U)[] ? IsSerializable : false; - -type PropsAreSerializable = - Exclude< - { - [K in keyof T]-?: IsFunction extends true ? false : IsSerializable; - }[keyof T], - true - > extends never - ? true - : false; - -type IsSpecialObject = T extends - | Date - | RegExp - | Map - | Set - | WeakMap - | WeakSet - ? true - : false; - -type IsSerializableObject = - IsFunction extends true - ? false - : IsArray extends true - ? false - : IsSpecialObject extends true - ? false - : T extends object - ? PropsAreSerializable - : false; - -export type IsSerializable = - IsAny extends true - ? false // discourage any; use explicit types - : IsNever extends true - ? true - : IsSerializablePrimitive extends true - ? true - : IsArray extends true - ? IsSerializableArray - : IsSerializableObject; - -// Public helper alias for consumers -export type Serializable = IsSerializable extends true ? T : never; - -// Human-readable reason per kind -type NonSerializableReason = - IsFunction extends true - ? "functions are not serializable" - : IsArray extends true - ? "array contains non-serializable element(s)" - : IsSpecialObject 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 = - IsSerializable extends true - ? T - : ["Non-serializable RPC argument", C, NonSerializableReason]; - -type IndexLabel = K extends string | number ? `${K}` : "?"; -type SerializableArgs = { - [K in keyof A]: EnsureSerializableWithMessage}]`>; -}; +// Serializability checks removed: transport and server decide value vs reference. diff --git a/libs/common/src/platform/services/sdk/rpc/protocol.ts b/libs/common/src/platform/services/sdk/rpc/protocol.ts index 399d9160bfa..bdd6b6a3ebd 100644 --- a/libs/common/src/platform/services/sdk/rpc/protocol.ts +++ b/libs/common/src/platform/services/sdk/rpc/protocol.ts @@ -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 }; diff --git a/libs/common/src/platform/services/sdk/rpc/proxies.ts b/libs/common/src/platform/services/sdk/rpc/proxies.ts index 16fdd14025e..f5608ec88ac 100644 --- a/libs/common/src/platform/services/sdk/rpc/proxies.ts +++ b/libs/common/src/platform/services/sdk/rpc/proxies.ts @@ -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 } { + 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 } { + 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); + return chain(p as Promise); }, }); } diff --git a/libs/common/src/platform/services/sdk/rpc/server.ts b/libs/common/src/platform/services/sdk/rpc/server.ts index e9258766cd7..79f58ec9520 100644 --- a/libs/common/src/platform/services/sdk/rpc/server.ts +++ b/libs/common/src/platform/services/sdk/rpc/server.ts @@ -36,6 +36,25 @@ export class RpcServer { } } + 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 { + 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) { diff --git a/libs/common/src/vault/services/totp.service.ts b/libs/common/src/vault/services/totp.service.ts index 180a10f40fe..04aac075e48 100644 --- a/libs/common/src/vault/services/totp.service.ts +++ b/libs/common/src/vault/services/totp.service.ts @@ -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 }), );