





























@@ -3,7 +3,8 @@ import fs from "node:fs/promises";
33import type { IncomingMessage } from "node:http";
44import os from "node:os";
55import path from "node:path";
6-import { describe, expect, it } from "vitest";
6+import { describe, expect, it, vi } from "vitest";
7+import { approveDevicePairing, requestDevicePairing } from "../infra/device-pairing.js";
78import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
89import type { ResolvedGatewayAuth } from "./auth.js";
910import { CONTROL_UI_BOOTSTRAP_CONFIG_PATH } from "./control-ui-contract.js";
@@ -264,6 +265,33 @@ describe("handleControlUiHttpRequest", () => {
264265}
265266}
266267268+async function withPairedOperatorDeviceToken<T>(params: { fn: (token: string) => Promise<T> }) {
269+const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-device-token-"));
270+vi.stubEnv("OPENCLAW_HOME", tempHome);
271+try {
272+const deviceId = "control-ui-device";
273+const requested = await requestDevicePairing({
274+ deviceId,
275+publicKey: "test-public-key",
276+role: "operator",
277+scopes: ["operator.read"],
278+clientId: "openclaw-control-ui",
279+clientMode: "webchat",
280+});
281+const approved = await approveDevicePairing(requested.request.requestId, {
282+callerScopes: ["operator.read"],
283+});
284+expect(approved?.status).toBe("approved");
285+const operatorToken =
286+approved?.status === "approved" ? approved.device.tokens?.operator?.token : undefined;
287+expect(typeof operatorToken).toBe("string");
288+return await params.fn(operatorToken ?? "");
289+} finally {
290+vi.unstubAllEnvs();
291+await fs.rm(tempHome, { recursive: true, force: true });
292+}
293+}
294+267295it("sets security headers for Control UI responses", async () => {
268296await withControlUiRoot({
269297fn: async (tmp) => {
@@ -370,6 +398,51 @@ describe("handleControlUiHttpRequest", () => {
370398});
371399});
372400401+it("accepts paired operator device tokens on assistant media requests", async () => {
402+await withPairedOperatorDeviceToken({
403+fn: async (operatorToken) => {
404+await withAllowedAssistantMediaRoot({
405+prefix: "ui-media-device-token-",
406+fn: async (tmpRoot) => {
407+const filePath = path.join(tmpRoot, "photo.png");
408+await fs.writeFile(filePath, Buffer.from("not-a-real-png"));
409+const { res, handled } = await runAssistantMediaRequest({
410+url: `/__openclaw__/assistant-media?source=${encodeURIComponent(filePath)}`,
411+method: "GET",
412+auth: { mode: "token", token: "shared-token", allowTailscale: false },
413+headers: {
414+authorization: `Bearer ${operatorToken}`,
415+},
416+});
417+expect(handled).toBe(true);
418+expect(res.statusCode).toBe(200);
419+},
420+});
421+},
422+});
423+});
424+425+it("accepts paired operator device tokens in assistant media query auth", async () => {
426+await withPairedOperatorDeviceToken({
427+fn: async (operatorToken) => {
428+await withAllowedAssistantMediaRoot({
429+prefix: "ui-media-device-token-query-",
430+fn: async (tmpRoot) => {
431+const filePath = path.join(tmpRoot, "photo.png");
432+await fs.writeFile(filePath, Buffer.from("not-a-real-png"));
433+const { res, handled } = await runAssistantMediaRequest({
434+url: `/__openclaw__/assistant-media?source=${encodeURIComponent(filePath)}&token=${encodeURIComponent(operatorToken)}`,
435+method: "GET",
436+auth: { mode: "token", token: "shared-token", allowTailscale: false },
437+});
438+expect(handled).toBe(true);
439+expect(res.statusCode).toBe(200);
440+},
441+});
442+},
443+});
444+});
445+373446it("rejects trusted-proxy assistant media requests from disallowed browser origins", async () => {
374447await withAllowedAssistantMediaRoot({
375448prefix: "ui-media-proxy-",
@@ -526,6 +599,28 @@ describe("handleControlUiHttpRequest", () => {
526599});
527600});
528601602+it("serves bootstrap config JSON when paired device-token auth is valid", async () => {
603+await withPairedOperatorDeviceToken({
604+fn: async (operatorToken) => {
605+await withControlUiRoot({
606+fn: async (tmp) => {
607+const { res, handled, end } = await runBootstrapConfigRequest({
608+rootPath: tmp,
609+auth: { mode: "token", token: "shared-token", allowTailscale: false },
610+headers: {
611+authorization: `Bearer ${operatorToken}`,
612+},
613+});
614+expect(handled).toBe(true);
615+expect(res.statusCode).toBe(200);
616+const parsed = parseBootstrapPayload(end);
617+expect(parsed.assistantAgentId).toBe("main");
618+},
619+});
620+},
621+});
622+});
623+529624it("serves bootstrap config JSON under basePath", async () => {
530625await withControlUiRoot({
531626fn: async (tmp) => {
@@ -618,6 +713,34 @@ describe("handleControlUiHttpRequest", () => {
618713}
619714});
620715716+it("serves local avatar bytes when paired device-token auth is valid", async () => {
717+await withPairedOperatorDeviceToken({
718+fn: async (operatorToken) => {
719+const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-avatar-device-token-"));
720+try {
721+const avatarPath = path.join(tmp, "main.png");
722+await fs.writeFile(avatarPath, "avatar-bytes\n");
723+724+const { res, handled, end } = await runAvatarRequest({
725+url: "/avatar/main",
726+method: "GET",
727+auth: { mode: "token", token: "shared-token", allowTailscale: false },
728+headers: {
729+authorization: `Bearer ${operatorToken}`,
730+},
731+resolveAvatar: () => ({ kind: "local", filePath: avatarPath }),
732+});
733+734+expect(handled).toBe(true);
735+expect(res.statusCode).toBe(200);
736+expect(String(end.mock.calls[0]?.[0] ?? "")).toBe("avatar-bytes\n");
737+} finally {
738+await fs.rm(tmp, { recursive: true, force: true });
739+}
740+},
741+});
742+});
743+621744it("returns avatar metadata when auth is enabled and the token is valid", async () => {
622745const { res, end, handled } = await runAvatarRequest({
623746url: "/avatar/main?meta=1",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。