diff --git a/src/gateway/server-methods/nodes.ts b/src/gateway/server-methods/nodes.ts index ecebab964..e434390d5 100644 --- a/src/gateway/server-methods/nodes.ts +++ b/src/gateway/server-methods/nodes.ts @@ -38,6 +38,24 @@ function isNodeEntry(entry: { role?: string; roles?: string[] }) { return false; } +function normalizeNodeInvokeResultParams(params: unknown): unknown { + if (!params || typeof params !== "object") return params; + const raw = params as Record; + const normalized: Record = { ...raw }; + if (normalized.payloadJSON === null) { + delete normalized.payloadJSON; + } else if (normalized.payloadJSON !== undefined && typeof normalized.payloadJSON !== "string") { + if (normalized.payload === undefined) { + normalized.payload = normalized.payloadJSON; + } + delete normalized.payloadJSON; + } + if (normalized.error === null) { + delete normalized.error; + } + return normalized; +} + export const nodeHandlers: GatewayRequestHandlers = { "node.pair.request": async ({ params, respond, context }) => { if (!validateNodePairRequestParams(params)) { @@ -417,7 +435,8 @@ export const nodeHandlers: GatewayRequestHandlers = { }); }, "node.invoke.result": async ({ params, respond, context, client }) => { - if (!validateNodeInvokeResultParams(params)) { + const normalizedParams = normalizeNodeInvokeResultParams(params); + if (!validateNodeInvokeResultParams(normalizedParams)) { respondInvalidParams({ respond, method: "node.invoke.result", @@ -425,7 +444,7 @@ export const nodeHandlers: GatewayRequestHandlers = { }); return; } - const p = params as { + const p = normalizedParams as { id: string; nodeId: string; ok: boolean; diff --git a/src/gateway/server.nodes.allowlist.test.ts b/src/gateway/server.nodes.allowlist.test.ts index c2b04ab25..b0aae5ab7 100644 --- a/src/gateway/server.nodes.allowlist.test.ts +++ b/src/gateway/server.nodes.allowlist.test.ts @@ -172,4 +172,55 @@ describe("gateway node command allowlist", () => { ws.close(); await server.close(); }); + + test("accepts node invoke result with null payloadJSON", async () => { + const { server, ws, port } = await startServerWithClient(); + await connectOk(ws); + + let resolveInvoke: ((payload: { id?: string; nodeId?: string }) => void) | null = null; + const invokeReqP = new Promise<{ id?: string; nodeId?: string }>((resolve) => { + resolveInvoke = resolve; + }); + const nodeClient = await connectNodeClient({ + port, + commands: ["canvas.snapshot"], + instanceId: "node-null-payloadjson", + displayName: "node-null-payloadjson", + onEvent: (evt) => { + if (evt.event === "node.invoke.request") { + const payload = evt.payload as { id?: string; nodeId?: string }; + resolveInvoke?.(payload); + } + }, + }); + + const listRes = await rpcReq<{ nodes?: Array<{ nodeId: string }> }>(ws, "node.list", {}); + const nodeId = listRes.payload?.nodes?.[0]?.nodeId ?? ""; + expect(nodeId).toBeTruthy(); + + const invokeResP = rpcReq(ws, "node.invoke", { + nodeId, + command: "canvas.snapshot", + params: { format: "png" }, + idempotencyKey: "allowlist-null-payloadjson", + }); + + const payload = await invokeReqP; + const requestId = payload?.id ?? ""; + const nodeIdFromReq = payload?.nodeId ?? "node-null-payloadjson"; + + await nodeClient.request("node.invoke.result", { + id: requestId, + nodeId: nodeIdFromReq, + ok: true, + payloadJSON: null, + }); + + const invokeRes = await invokeResP; + expect(invokeRes.ok).toBe(true); + + nodeClient.stop(); + ws.close(); + await server.close(); + }); });