From 15e3a2a395fb77818b608574caa04ccb130b5deb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 21 Jan 2026 04:13:34 +0000 Subject: [PATCH] fix: sanitize node invoke result params --- src/node-host/runner.test.ts | 34 +++++++++++++++++++++++++ src/node-host/runner.ts | 49 ++++++++++++++++++++++++++++++------ 2 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 src/node-host/runner.test.ts diff --git a/src/node-host/runner.test.ts b/src/node-host/runner.test.ts new file mode 100644 index 000000000..9d89a0097 --- /dev/null +++ b/src/node-host/runner.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "vitest"; + +import { buildNodeInvokeResultParams } from "./runner.js"; + +describe("buildNodeInvokeResultParams", () => { + test("omits optional fields when null/undefined", () => { + const params = buildNodeInvokeResultParams( + { id: "invoke-1", nodeId: "node-1", command: "system.run" }, + { ok: true, payloadJSON: null, error: null }, + ); + + expect(params).toEqual({ id: "invoke-1", nodeId: "node-1", ok: true }); + expect("payloadJSON" in params).toBe(false); + expect("error" in params).toBe(false); + }); + + test("includes payloadJSON when provided", () => { + const params = buildNodeInvokeResultParams( + { id: "invoke-2", nodeId: "node-2", command: "system.run" }, + { ok: true, payloadJSON: '{"ok":true}' }, + ); + + expect(params.payloadJSON).toBe('{"ok":true}'); + }); + + test("includes payload when provided", () => { + const params = buildNodeInvokeResultParams( + { id: "invoke-3", nodeId: "node-3", command: "system.run" }, + { ok: false, payload: { reason: "bad" } }, + ); + + expect(params.payload).toEqual({ reason: "bad" }); + }); +}); diff --git a/src/node-host/runner.ts b/src/node-host/runner.ts index 6093beb77..d9da98750 100644 --- a/src/node-host/runner.ts +++ b/src/node-host/runner.ts @@ -833,19 +833,52 @@ async function sendInvokeResult( }, ) { try { - await client.request("node.invoke.result", { - id: frame.id, - nodeId: frame.nodeId, - ok: result.ok, - payload: result.payload, - payloadJSON: result.payloadJSON ?? null, - error: result.error ?? null, - }); + await client.request("node.invoke.result", buildNodeInvokeResultParams(frame, result)); } catch { // ignore: node invoke responses are best-effort } } +export function buildNodeInvokeResultParams( + frame: NodeInvokeRequestPayload, + result: { + ok: boolean; + payload?: unknown; + payloadJSON?: string | null; + error?: { code?: string; message?: string } | null; + }, +): { + id: string; + nodeId: string; + ok: boolean; + payload?: unknown; + payloadJSON?: string; + error?: { code?: string; message?: string }; +} { + const params: { + id: string; + nodeId: string; + ok: boolean; + payload?: unknown; + payloadJSON?: string; + error?: { code?: string; message?: string }; + } = { + id: frame.id, + nodeId: frame.nodeId, + ok: result.ok, + }; + if (result.payload !== undefined) { + params.payload = result.payload; + } + if (typeof result.payloadJSON === "string") { + params.payloadJSON = result.payloadJSON; + } + if (result.error) { + params.error = result.error; + } + return params; +} + async function sendNodeEvent(client: GatewayClient, event: string, payload: unknown) { try { await client.request("node.event", {