fix(apps): track the latest accepted app ui/initialize, not the first
The previous round froze the recorded capabilities at the first handshake, on the belief that the bridge ignores re-initialization. It does not: ext-apps 1.7.5's `_oninitialize` warns about the double-mount and then assigns `_appCapabilities` and `_appInfo` from the new frame — "the latest appInfo/ appCapabilities replace the previous values", in its own words. Freezing left this gate reporting capabilities the bridge no longer held, in both directions: a handshake advertising `elicitation` followed by one without it still read as advertised. Every frame that passes the accept gate now replaces the recorded value, and `appCapabilities` joins `protocolVersion` and `appInfo` in that gate since the bridge's own schema requires all three. A frame the bridge would reject still records nothing AND leaves the previous value alone — it is a route to changing the gate in neither direction. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
cc0770babd
commit
b86db5c7a2
2 changed files with 46 additions and 17 deletions
|
|
@ -141,15 +141,39 @@ describe("appCapabilities (#1854)", () => {
|
|||
expect(appAdvertisesElicitation(bridge)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the capabilities from the first handshake, not a later frame", () => {
|
||||
// The bridge keeps what it accepted and ignores re-initialization; this
|
||||
// gate has to agree, or a second frame could change what it reports.
|
||||
it("takes the latest accepted handshake, in both directions", () => {
|
||||
// Verified against ext-apps 1.7.5: a second `ui/initialize` is accepted —
|
||||
// the bridge warns about the double-mount and the latest appInfo and
|
||||
// capabilities REPLACE the previous ones. Freezing this at the first frame
|
||||
// would leave the gate reporting capabilities the bridge no longer holds.
|
||||
const bridge = makeBridge({});
|
||||
const { transport } = makeTransport();
|
||||
observeAppCapabilities(bridge, transport);
|
||||
|
||||
transport.onmessage?.(initializeFrame({ availableDisplayModes: [] }));
|
||||
transport.onmessage?.(initializeFrame({ elicitation: {} }));
|
||||
expect(appAdvertisesElicitation(bridge)).toBe(false);
|
||||
transport.onmessage?.(initializeFrame({ elicitation: {} }));
|
||||
expect(appAdvertisesElicitation(bridge)).toBe(true);
|
||||
// …and a re-handshake that drops the capability turns it back off.
|
||||
transport.onmessage?.(initializeFrame({}));
|
||||
expect(appAdvertisesElicitation(bridge)).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves a recorded capability alone when a later frame is malformed", () => {
|
||||
// A rejected frame is not a route to changing the gate — in either
|
||||
// direction: it cannot set `elicitation`, and it cannot clear one the
|
||||
// bridge still holds.
|
||||
const bridge = makeBridge({});
|
||||
const { transport } = makeTransport();
|
||||
observeAppCapabilities(bridge, transport);
|
||||
transport.onmessage?.(initializeFrame({ elicitation: {} }));
|
||||
transport.onmessage?.({
|
||||
jsonrpc: "2.0",
|
||||
id: 9,
|
||||
method: "ui/initialize",
|
||||
params: { appCapabilities: {} },
|
||||
} as unknown as JSONRPCMessage);
|
||||
expect(appAdvertisesElicitation(bridge)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps bridges independent", () => {
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ export interface MessageObservable<TMessage = unknown> {
|
|||
|
||||
/**
|
||||
* Whether `message` is a well-formed `ui/initialize` REQUEST — a JSON-RPC
|
||||
* request id, plus the two params the handshake requires (`protocolVersion`,
|
||||
* `appInfo`).
|
||||
* request id, plus the three params the handshake requires
|
||||
* (`protocolVersion`, `appInfo`, `appCapabilities`).
|
||||
*
|
||||
* This is a gate, not a second copy of the bridge's schema: the bridge stays
|
||||
* the authority on everything else in the frame. What it has to stop is a
|
||||
|
|
@ -52,7 +52,7 @@ export interface MessageObservable<TMessage = unknown> {
|
|||
*/
|
||||
function isInitializeRequest(
|
||||
message: unknown,
|
||||
): message is { params: { appCapabilities?: unknown } } {
|
||||
): message is { params: { appCapabilities: Record<string, unknown> } } {
|
||||
if (typeof message !== "object" || message === null) return false;
|
||||
const frame = message as { id?: unknown; method?: unknown; params?: unknown };
|
||||
if (frame.method !== "ui/initialize") return false;
|
||||
|
|
@ -62,33 +62,38 @@ function isInitializeRequest(
|
|||
const params = frame.params as {
|
||||
protocolVersion?: unknown;
|
||||
appInfo?: unknown;
|
||||
appCapabilities?: unknown;
|
||||
};
|
||||
// All three are required by the bridge's own initialize schema, so a frame
|
||||
// missing any of them is one the bridge rejects.
|
||||
return (
|
||||
typeof params.protocolVersion === "string" &&
|
||||
typeof params.appInfo === "object" &&
|
||||
params.appInfo !== null
|
||||
params.appInfo !== null &&
|
||||
typeof params.appCapabilities === "object" &&
|
||||
params.appCapabilities !== null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record `params.appCapabilities` from the view's handshake.
|
||||
*
|
||||
* Only the FIRST well-formed `ui/initialize` is recorded, mirroring the bridge:
|
||||
* it keeps the capabilities it accepted and ignores re-initialization, so a
|
||||
* later frame — malformed or not — must not be able to change what this gate
|
||||
* reports either. Fail-closed in both directions: a malformed first frame
|
||||
* records nothing.
|
||||
* Every *accepted* `ui/initialize` replaces the recorded value, because that is
|
||||
* what the bridge does — on a second handshake (a view double-mounting under
|
||||
* React StrictMode, or reconnecting) it warns and takes the latest appInfo and
|
||||
* capabilities. Freezing this at the first frame would leave the gate reporting
|
||||
* capabilities the bridge no longer holds, in both directions.
|
||||
*
|
||||
* A frame the bridge would *reject* records nothing and leaves the previous
|
||||
* value alone, which is what keeps the gate fail-closed: a malformed initialize
|
||||
* must not be a second, laxer route to setting `elicitation`.
|
||||
*/
|
||||
function recordAdvertisedCapabilities(
|
||||
bridge: AppBridge,
|
||||
message: unknown,
|
||||
): void {
|
||||
if (rawAppCapabilities.has(bridge)) return;
|
||||
if (!isInitializeRequest(message)) return;
|
||||
const advertised = message.params.appCapabilities;
|
||||
if (typeof advertised === "object" && advertised !== null) {
|
||||
rawAppCapabilities.set(bridge, advertised as Record<string, unknown>);
|
||||
}
|
||||
rawAppCapabilities.set(bridge, message.params.appCapabilities);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue