fix(ui): route all /open-cmux callers through postOpenCmux (OPR.0.4.0.1)

Guard blocker: consolidated all 4 /open-cmux UI callers through the
shared postOpenCmux helper (LiveNodeDetails, RigNode, LaunchCmuxButton
+ useCmuxLaunch). No direct fetch to /open-cmux remains in consumers.

postOpenCmux owns the URL construction + terminalAuthHeaders(), so
the runtime fetch-intercept test for postOpenCmux covers all callers.

Source guard: verifies all 3 consumer files import postOpenCmux and
have no direct fetch to /open-cmux. Fails closed on zero matches.

7/7 token-carry + 51/51 UI. tsc 0.
This commit is contained in:
mvschwarz committed 2026-06-18 08:19:36 -07:00
1 parent 74541194c7
commit 342cd85cfa
4 files changed
+33 -44

No files matched your search

@@ -48,7 +48,7 @@ import {
import { getRestoreStatusColorClass } from "../lib/restore-status-colors.js";
import type { AgentSpecReview } from "../hooks/useSpecReview.js";
import { RuntimeBadge, ToolMark } from "./graphics/RuntimeMark.js";
import { terminalAuthHeaders } from "./mission-control/missionControlAuth.js";
import { postOpenCmux } from "../hooks/useCmuxLaunch.js";
type Tab = "overview" | "details";
@@ -188,10 +188,7 @@ function ActionButtonsRow({ rigId, logicalId, data }: { rigId: string; logicalId
};
const handleOpenCmux = async () => {
try {
await fetch(
`/api/rigs/${encodeURIComponent(rigId)}/nodes/${encodeURIComponent(logicalId)}/open-cmux`,
{ method: "POST", headers: terminalAuthHeaders() },
);
await postOpenCmux({ rigId, logicalId });
} catch {
// best effort
}
+3 -5
View File
@@ -18,7 +18,7 @@ import { TerminalPreviewPopover } from "./topology/TerminalPreviewPopover.js";
import type { TopologyActivityVisual } from "../lib/topology-activity.js";
import { formatCompactTokenCount, formatTokenTotalTitle, sumTokenCounts } from "../lib/token-format.js";
import { formatRuntimeModel } from "../lib/runtime-brand.js";
import { terminalAuthHeaders } from "./mission-control/missionControlAuth.js";
import { postOpenCmux } from "../hooks/useCmuxLaunch.js";
import { RuntimeBadge, ToolMark } from "./graphics/RuntimeMark.js";
interface RigNodeData {
@@ -143,10 +143,8 @@ export function RigNode({ data }: { data: RigNodeData }) {
e.stopPropagation();
if (!data.rigId) return;
try {
const res = await fetch(`/api/rigs/${encodeURIComponent(data.rigId)}/nodes/${encodeURIComponent(data.logicalId)}/open-cmux`, { method: "POST", headers: terminalAuthHeaders() });
if (res.ok) {
flashFeedback("cmux");
}
const result = await postOpenCmux({ rigId: data.rigId, logicalId: data.logicalId });
if (result.ok) flashFeedback("cmux");
} catch { /* best-effort */ }
};
@@ -11,7 +11,7 @@
import { useEffect, useRef } from "react";
import { launchRigCmux } from "../../hooks/launchRigCmux.js";
import { terminalAuthHeaders } from "../mission-control/missionControlAuth.js";
import { postOpenCmux } from "../../hooks/useCmuxLaunch.js";
interface LaunchCmuxButtonProps {
rigId: string;
@@ -140,10 +140,12 @@ export function LaunchCmuxButton({ rigId }: LaunchCmuxButtonProps) {
if (openMissingRef.current) openMissingRef.current.disabled = true;
Promise.allSettled(
ids.map(async (logicalId) => {
const res = await fetch(`/api/rigs/${encodeURIComponent(rigId)}/nodes/${encodeURIComponent(logicalId)}/open-cmux`, { method: "POST", headers: terminalAuthHeaders() });
if (!res.ok) return { ok: false };
const body = await res.json().catch(() => ({})) as { ok?: boolean };
return { ok: body.ok === true };
try {
const result = await postOpenCmux({ rigId, logicalId });
return { ok: result.ok === true };
} catch {
return { ok: false };
}
}),
).then((results) => {
const opened = results.filter((r) => r.status === "fulfilled" && (r as PromiseFulfilledResult<{ ok: boolean }>).value.ok).length;
+21 -29
View File
@@ -44,17 +44,6 @@ describe("terminal-token carry on protected UI fetches", () => {
expect(headers?.Authorization).toBe(`Bearer ${TOKEN}`);
});
it("terminalAuthHeaders reads token from localStorage", async () => {
const { terminalAuthHeaders } = await import("../src/components/mission-control/missionControlAuth.js");
expect(terminalAuthHeaders().Authorization).toBe(`Bearer ${TOKEN}`);
});
it("terminalAuthHeaders returns empty when no token", async () => {
window.localStorage.removeItem("openrig.terminalBearerToken");
const { terminalAuthHeaders } = await import("../src/components/mission-control/missionControlAuth.js");
expect(terminalAuthHeaders().Authorization).toBeUndefined();
});
it("postOpenCmux sends Authorization: Bearer <token>", async () => {
const { calls } = stubFetch();
const { postOpenCmux } = await import("../src/hooks/useCmuxLaunch.js");
@@ -66,37 +55,40 @@ describe("terminal-token carry on protected UI fetches", () => {
expect(headers?.Authorization).toBe(`Bearer ${TOKEN}`);
});
it("source guard: all /open-cmux fetches carry terminalAuthHeaders", async () => {
it("terminalAuthHeaders reads token from localStorage", async () => {
const { terminalAuthHeaders } = await import("../src/components/mission-control/missionControlAuth.js");
expect(terminalAuthHeaders().Authorization).toBe(`Bearer ${TOKEN}`);
});
it("terminalAuthHeaders returns empty when no token", async () => {
window.localStorage.removeItem("openrig.terminalBearerToken");
const { terminalAuthHeaders } = await import("../src/components/mission-control/missionControlAuth.js");
expect(terminalAuthHeaders().Authorization).toBeUndefined();
});
it("source guard: all /open-cmux consumers use postOpenCmux (no direct fetch)", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const srcDir = path.resolve(import.meta.dirname, "../src");
const files = [
"hooks/useCmuxLaunch.ts",
"components/topology/LaunchCmuxButton.tsx",
const consumers = [
"components/LiveNodeDetails.tsx",
"components/RigNode.tsx",
"components/topology/LaunchCmuxButton.tsx",
];
for (const file of files) {
for (const file of consumers) {
const src = fs.readFileSync(path.join(srcDir, file), "utf-8");
const cmuxFetches = [...src.matchAll(/fetch\([^)]*open-cmux[^)]*\)/gs)];
for (const match of cmuxFetches) {
expect(match[0]).toContain("terminalAuthHeaders()");
}
expect(src, `${file} must import postOpenCmux`).toContain("postOpenCmux");
const lines = src.split("\n");
const directFetchLines = lines.filter((l) => l.includes("open-cmux") && l.includes("fetch("));
expect(directFetchLines.length, `${file} has direct fetch to /open-cmux`).toBe(0);
}
});
it("source guard: useCmuxLaunch wires terminalAuthHeaders", async () => {
it("source guard: postOpenCmux owns the /open-cmux fetch with terminalAuthHeaders", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const src = fs.readFileSync(path.resolve(import.meta.dirname, "../src/hooks/useCmuxLaunch.ts"), "utf-8");
expect(src).toContain("terminalAuthHeaders()");
expect(src).toContain("headers: terminalAuthHeaders()");
});
it("source guard: LaunchCmuxButton wires terminalAuthHeaders", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const src = fs.readFileSync(path.resolve(import.meta.dirname, "../src/components/topology/LaunchCmuxButton.tsx"), "utf-8");
expect(src).toContain("headers: terminalAuthHeaders()");
expect(src).toContain("open-cmux");
});
});