fix: keep Browser RPC collaborator input and output inside the contract
RPC-RR-02. The server-stream path captured the generation fence outside its protected boundary and raceWithin invoked clock.sleep outside a promise boundary, so a synchronous throw from either escaped the Result contract and skipped the listener and timer release. Both now run inside the boundary, and release moved to finally. RPC-RR-03. The runtime snapshotted its transports only after validating the caller's raw objects, which ran their accessors first. It now decodes the registry from own data descriptors before anything reads it — refusing an accessor without invoking it and rejecting extra, inherited and symbol-keyed fields — and validates that snapshot. Every installed binding registry is a read facade over a private store instead of a frozen Map whose set, delete and clear still worked. RPC-RR-04. Transport results and stream frames are decoded per union variant from own data descriptors into new frozen values. A throwing getter, an inherited or extra field, a symbol key, an unknown failure code and an out-of-range retryAfterMs all close as protocol failures instead of escaping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6a8281a941
commit
bd90e0c983
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
createReadOnlyRegistry,
|
||||
type ReadOnlyRegistry,
|
||||
} from "./read-only-registry.ts";
|
||||
import type { InstalledBoundaryMapper } from "./boundary-mapper.ts";
|
||||
import type { RuntimeSchemaCodec } from "./schema-registry.ts";
|
||||
|
||||
@@ -327,13 +331,18 @@ export function composeBrowserRpcRequestEncoderRegistry(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC-RR-03. Read facades, never `Map`s. `Object.freeze(new Map(...))` leaves
|
||||
* `set`, `delete` and `clear` working, so an installed registry could still be
|
||||
* emptied or re-pointed after the snapshot was validated.
|
||||
*/
|
||||
export type InstalledBrowserRpcContractBindings = Readonly<{
|
||||
operations: ReadonlyMap<string, BrowserRpcOperationV3>;
|
||||
profiles: ReadonlyMap<string, BrowserRpcProviderProfile>;
|
||||
schemaCodecs: ReadonlyMap<string, RuntimeSchemaCodec>;
|
||||
mappers: ReadonlyMap<string, InstalledBoundaryMapper>;
|
||||
requestEncoders: ReadonlyMap<string, BrowserRpcRequestEncoder>;
|
||||
runtimeBindings: ReadonlyMap<string, BrowserRpcRuntimeBindingIdentity>;
|
||||
operations: ReadOnlyRegistry<string, BrowserRpcOperationV3>;
|
||||
profiles: ReadOnlyRegistry<string, BrowserRpcProviderProfile>;
|
||||
schemaCodecs: ReadOnlyRegistry<string, RuntimeSchemaCodec>;
|
||||
mappers: ReadOnlyRegistry<string, InstalledBoundaryMapper>;
|
||||
requestEncoders: ReadOnlyRegistry<string, BrowserRpcRequestEncoder>;
|
||||
runtimeBindings: ReadOnlyRegistry<string, BrowserRpcRuntimeBindingIdentity>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
@@ -351,7 +360,7 @@ function installRegistrySnapshot<Value extends object>(
|
||||
source: Readonly<Record<string, Value>>,
|
||||
label: string,
|
||||
allowedKeys: readonly string[],
|
||||
): ReadonlyMap<string, Value> {
|
||||
): ReadOnlyRegistry<string, Value> {
|
||||
let ownKeys: string[];
|
||||
let symbols: readonly symbol[];
|
||||
try {
|
||||
@@ -376,7 +385,7 @@ function installRegistrySnapshot<Value extends object>(
|
||||
installRowSnapshot(descriptor.value as Value, `${label}.${key}`, allowedKeys),
|
||||
);
|
||||
}
|
||||
return Object.freeze(installed) as ReadonlyMap<string, Value>;
|
||||
return createReadOnlyRegistry(installed);
|
||||
}
|
||||
|
||||
function installRowSnapshot<Value extends object>(
|
||||
|
||||
Reference in New Issue
Block a user