



















@@ -1,6 +1,7 @@
11import crypto from "node:crypto";
22import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
33import { WebSocket } from "ws";
4+import { writeConfigFile } from "../config/config.js";
45import {
56deriveDeviceIdFromPublicKey,
67type DeviceIdentity,
@@ -174,7 +175,17 @@ describe("node.invoke approval bypass", () => {
174175let port: number;
175176176177beforeAll(async () => {
177-const started = await startServerWithClient("secret", { controlUiEnabled: true });
178+await writeConfigFile({
179+gateway: {
180+nodes: {
181+pairing: { autoApproveCidrs: ["127.0.0.1/32", "::1/128"] },
182+allowCommands: ["system.run", "system.run.prepare", "system.which"],
183+},
184+},
185+});
186+const started = await startServerWithClient("secret", {
187+controlUiEnabled: true,
188+});
178189server = started.server;
179190port = started.port;
180191started.ws.close();
@@ -186,12 +197,19 @@ describe("node.invoke approval bypass", () => {
186197187198const approveAllPendingPairings = async () => {
188199const { approveDevicePairing, listDevicePairing } = await import("../infra/device-pairing.js");
189-const list = await listDevicePairing();
190-for (const pending of list.pending) {
200+const { approveNodePairing, listNodePairing } = await import("../infra/node-pairing.js");
201+const deviceList = await listDevicePairing();
202+for (const pending of deviceList.pending) {
191203await approveDevicePairing(pending.requestId, {
192204callerScopes: pending.scopes ?? ["operator.admin"],
193205});
194206}
207+const nodeList = await listNodePairing();
208+for (const pending of nodeList.pending) {
209+await approveNodePairing(pending.requestId, {
210+callerScopes: ["operator.admin"],
211+});
212+}
195213};
196214197215const connectOperatorWithRetry = async (
@@ -299,67 +317,75 @@ describe("node.invoke approval bypass", () => {
299317deviceIdentity?: DeviceIdentity,
300318commands: string[] = ["system.run"],
301319) => {
302-let readyResolve: (() => void) | null = null;
303-const ready = new Promise<void>((resolve) => {
304-readyResolve = resolve;
305-});
306-307320const resolvedDeviceIdentity = deviceIdentity ?? createDeviceIdentity();
308-const client = new GatewayClient({
309-url: `ws://127.0.0.1:${port}`,
310-// Keep challenge timeout realistic in tests; 0 maps to a 250ms timeout and can
311-// trigger reconnect backoff loops under load.
312-connectChallengeTimeoutMs: 2_000,
313-token: "secret",
314-role: "node",
315-clientName: GATEWAY_CLIENT_NAMES.NODE_HOST,
316-clientVersion: "1.0.0",
317-platform: "linux",
318-mode: GATEWAY_CLIENT_MODES.NODE,
319-scopes: [],
320- commands,
321-deviceIdentity: resolvedDeviceIdentity,
322-onHelloOk: () => readyResolve?.(),
323-onEvent: (evt) => {
324-if (evt.event !== "node.invoke.request") {
325-return;
326-}
327-onInvoke(evt.payload);
328-const payload = evt.payload as {
329-id?: string;
330-nodeId?: string;
331-};
332-const id = typeof payload?.id === "string" ? payload.id : "";
333-const nodeId = typeof payload?.nodeId === "string" ? payload.nodeId : "";
334-if (!id || !nodeId) {
335-return;
321+322+const startNodeClient = async () => {
323+let readyResolve: (() => void) | null = null;
324+const ready = new Promise<void>((resolve) => {
325+readyResolve = resolve;
326+});
327+const client = new GatewayClient({
328+url: `ws://127.0.0.1:${port}`,
329+// Keep challenge timeout realistic in tests; 0 maps to a 250ms timeout and can
330+// trigger reconnect backoff loops under load.
331+connectChallengeTimeoutMs: 2_000,
332+token: "secret",
333+role: "node",
334+clientName: GATEWAY_CLIENT_NAMES.NODE_HOST,
335+clientVersion: "1.0.0",
336+platform: "linux",
337+mode: GATEWAY_CLIENT_MODES.NODE,
338+scopes: [],
339+caps: ["system"],
340+ commands,
341+deviceIdentity: resolvedDeviceIdentity,
342+onHelloOk: () => readyResolve?.(),
343+onEvent: (evt) => {
344+if (evt.event !== "node.invoke.request") {
345+return;
346+}
347+onInvoke(evt.payload);
348+const payload = evt.payload as {
349+id?: string;
350+nodeId?: string;
351+};
352+const id = typeof payload?.id === "string" ? payload.id : "";
353+const nodeId = typeof payload?.nodeId === "string" ? payload.nodeId : "";
354+if (!id || !nodeId) {
355+return;
356+}
357+void client.request("node.invoke.result", {
358+ id,
359+ nodeId,
360+ok: true,
361+payloadJSON: JSON.stringify({ ok: true }),
362+});
363+},
364+});
365+client.start();
366+let timer: NodeJS.Timeout | undefined;
367+try {
368+await Promise.race([
369+ready,
370+new Promise<never>((_, reject) => {
371+timer = setTimeout(
372+() => reject(new Error("timeout waiting for node to connect")),
373+NODE_CONNECT_TIMEOUT_MS,
374+);
375+}),
376+]);
377+} finally {
378+if (timer) {
379+clearTimeout(timer);
336380}
337-void client.request("node.invoke.result", {
338- id,
339- nodeId,
340-ok: true,
341-payloadJSON: JSON.stringify({ ok: true }),
342-});
343-},
344-});
345-client.start();
346-let timer: NodeJS.Timeout | undefined;
347-try {
348-await Promise.race([
349-ready,
350-new Promise<never>((_, reject) => {
351-timer = setTimeout(
352-() => reject(new Error("timeout waiting for node to connect")),
353-NODE_CONNECT_TIMEOUT_MS,
354-);
355-}),
356-]);
357-} finally {
358-if (timer) {
359-clearTimeout(timer);
360381}
361-}
362-return client;
382+return client;
383+};
384+385+const pendingClient = await startNodeClient();
386+await approveAllPendingPairings();
387+pendingClient.stop();
388+return await startNodeClient();
363389};
364390365391test("rejects malformed/forbidden node.invoke payloads before forwarding", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。