






















@@ -331,7 +331,7 @@ export function registerControlUiAndPairingSuite(): void {
331331});
332332});
333333334-test("preserves trusted-proxy control ui scopes for unpaired device identity", async () => {
334+test("requires pairing for trusted-proxy control ui device identity", async () => {
335335const { replaceConfigFile } = await import("../config/config.js");
336336testState.gatewayAuth = undefined;
337337testState.gatewayControlUi = {
@@ -375,17 +375,64 @@ export function registerControlUiAndPairingSuite(): void {
375375 device,
376376client: { ...CONTROL_UI_CLIENT },
377377});
378+expect(res.ok).toBe(false);
379+expect(res.error?.message ?? "").toContain("pairing required");
380+expect((res.error?.details as { code?: string } | undefined)?.code).toBe(
381+ConnectErrorDetailCodes.PAIRING_REQUIRED,
382+);
383+} finally {
384+ws.close();
385+}
386+});
387+});
388+389+test("clears trusted-proxy control ui scopes without device identity", async () => {
390+const { replaceConfigFile } = await import("../config/config.js");
391+testState.gatewayAuth = undefined;
392+testState.gatewayControlUi = {
393+ ...testState.gatewayControlUi,
394+allowedOrigins: ["https://localhost"],
395+};
396+await replaceConfigFile({
397+nextConfig: {
398+gateway: {
399+auth: {
400+mode: "trusted-proxy",
401+trustedProxy: {
402+userHeader: "x-forwarded-user",
403+requiredHeaders: ["x-forwarded-proto"],
404+allowLoopback: true,
405+},
406+},
407+trustedProxies: ["127.0.0.1"],
408+controlUi: {
409+allowedOrigins: ["https://localhost"],
410+},
411+},
412+},
413+afterWrite: { mode: "auto" },
414+});
415+await withControlUiGatewayServer(async ({ port }) => {
416+const ws = await openWs(port, TRUSTED_PROXY_CONTROL_UI_HEADERS);
417+try {
418+const res = await connectReq(ws, {
419+skipDefaultAuth: true,
420+scopes: ["operator.admin", "operator.read"],
421+device: null,
422+client: { ...CONTROL_UI_CLIENT },
423+});
378424expect(res.ok).toBe(true);
379425const payload = res.payload as
380426| {
381427auth?: { scopes?: string[]; deviceToken?: string };
382428}
383429| undefined;
384-expect(payload?.auth?.scopes).toEqual(["operator.admin", "operator.read"]);
430+expect(payload?.auth?.scopes).toEqual([]);
385431expect(payload?.auth?.deviceToken).toBeUndefined();
386432387433const admin = await rpcReq(ws, "set-heartbeats", { enabled: false });
388-expect(admin.ok).toBe(true);
434+expect(admin.ok).toBe(false);
435+expect(admin.error?.message ?? "").toContain("missing scope");
389436} finally {
390437ws.close();
391438}
@@ -419,6 +466,14 @@ export function registerControlUiAndPairingSuite(): void {
419466afterWrite: { mode: "auto" },
420467});
421468await withControlUiGatewayServer(async ({ port }) => {
469+const seeded = await seedApprovedOperatorReadPairing({
470+identityPrefix: "openclaw-control-ui-trusted-proxy-bounded-",
471+clientId: CONTROL_UI_CLIENT.id,
472+clientMode: CONTROL_UI_CLIENT.mode,
473+displayName: "Control UI",
474+platform: "web",
475+scopes: ["operator.admin", "operator.read"],
476+});
422477const ws = await openWs(port, {
423478 ...TRUSTED_PROXY_CONTROL_UI_HEADERS,
424479"x-openclaw-scopes": "operator.read",
@@ -431,6 +486,7 @@ export function registerControlUiAndPairingSuite(): void {
431486scopes: ["operator.admin", "operator.read"],
432487clientId: CONTROL_UI_CLIENT.id,
433488clientMode: CONTROL_UI_CLIENT.mode,
489+identityPath: seeded.identityPath,
434490nonce: challengeNonce,
435491});
436492const res = await connectReq(ws, {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。