import os from "node:os";
import path from "node:path";
import { expect, test, vi } from "vitest";
import { WebSocket } from "ws";
import {
  BACKEND_GATEWAY_CLIENT,
  connectReq,
  configureTrustedProxyControlUiAuth,
  CONTROL_UI_CLIENT,
  ConnectErrorDetailCodes,
  createSignedDevice,
  ensurePairedDeviceTokenForCurrentIdentity,
  GATEWAY_CLIENT_MODES,
  GATEWAY_CLIENT_NAMES,
  onceMessage,
  openTailscaleWs,
  openWs,
  originForPort,
  readConnectChallengeNonce,
  restoreGatewayToken,
  rpcReq,
  startRateLimitedTokenServerWithPairedDeviceToken,
  startGatewayServer,
  startServer,
  startServerWithClient,
  TEST_OPERATOR_CLIENT,
  testState,
  TRUSTED_PROXY_CONTROL_UI_HEADERS,
  waitForWsClose,
  withGatewayServer,
  writeTrustedProxyControlUiConfig,
} from "./server.auth.shared.js";

const operatorIdentityPathByPrefix = new Map<string, string>();

function expectArrayIncludes(actual: unknown, expectedValues: string[]): void {
  expect(Array.isArray(actual)).toBe(true);
  const values = actual as unknown[];
  for (const expected of expectedValues) {
    expect(values).toContain(expected);
  }
}

export function registerControlUiAndPairingSuite(): void {
  const trustedProxyControlUiCases: Array<{
    name: string;
    role: "operator" | "node";
    withUnpairedNodeDevice: boolean;
    expectedOk: boolean;
    expectedErrorSubstring?: string;
    expectedErrorCode?: string;
  }> = [
    {
      name: "rejects loopback trusted-proxy control ui operator without device identity",
      role: "operator",
      withUnpairedNodeDevice: false,
      expectedOk: false,
      expectedErrorSubstring: "control ui requires device identity",
      expectedErrorCode: ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
    },
    {
      name: "rejects trusted-proxy control ui node role without device identity",
      role: "node",
      withUnpairedNodeDevice: false,
      expectedOk: false,
      expectedErrorSubstring: "control ui requires device identity",
      expectedErrorCode: ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
    },
    {
      name: "rejects loopback trusted-proxy control ui node role before pairing",
      role: "node",
      withUnpairedNodeDevice: true,
      expectedOk: false,
      expectedErrorSubstring: "unauthorized",
    },
  ];

  const buildSignedDeviceForIdentity = async (params: {
    identityPath: string;
    client: { id: string; mode: string };
    nonce: string;
    scopes: string[];
    role?: "operator" | "node";
  }) => {
    const { device } = await createSignedDevice({
      token: "secret",
      scopes: params.scopes,
      clientId: params.client.id,
      clientMode: params.client.mode,
      role: params.role ?? "operator",
      identityPath: params.identityPath,
      nonce: params.nonce,
    });
    return device;
  };

  const REMOTE_BOOTSTRAP_HEADERS = {
    "x-forwarded-for": "10.0.0.14",
  };

  const expectStatusAndHealthOk = async (ws: WebSocket) => {
    const status = await rpcReq(ws, "status");
    expect(status.ok).toBe(true);
    const health = await rpcReq(ws, "health");
    expect(health.ok).toBe(true);
  };

  const expectAdminRpcOk = async (ws: WebSocket) => {
    const admin = await rpcReq(ws, "set-heartbeats", { enabled: false });
    expect(admin.ok).toBe(true);
  };

  const connectControlUiWithoutDeviceAndExpectOk = async (params: {
    ws: WebSocket;
    token?: string;
    password?: string;
    client?: { id: string; version: string; platform: string; mode: string };
  }) => {
    const res = await connectReq(params.ws, {
      ...(params.token ? { token: params.token } : {}),
      ...(params.password ? { password: params.password } : {}),
      device: null,
      client: { ...(params.client ?? CONTROL_UI_CLIENT) },
    });
    expect(res.ok).toBe(true);
    await expectStatusAndHealthOk(params.ws);
    await expectAdminRpcOk(params.ws);
  };

  const createOperatorIdentityFixture = async (identityPrefix: string) => {
    const { loadOrCreateDeviceIdentity } = await import("../infra/device-identity.js");
    let identityPath = operatorIdentityPathByPrefix.get(identityPrefix);
    if (!identityPath) {
      const poolId = process.env.VITEST_POOL_ID ?? "0";
      identityPath = path.join(os.tmpdir(), `${identityPrefix}${process.pid}-${poolId}.json`);
      operatorIdentityPathByPrefix.set(identityPrefix, identityPath);
    }
    const identity = loadOrCreateDeviceIdentity(identityPath);
    return {
      identityPath,
      identity,
      client: { ...TEST_OPERATOR_CLIENT },
    };
  };

  const startControlUiServerWithOperatorIdentity = async (
    identityPrefix = "openclaw-device-scope-",
  ) => {
    const { server, port, prevToken } = await startControlUiServer("secret");
    const { identityPath, identity, client } = await createOperatorIdentityFixture(identityPrefix);
    return { server, port, prevToken, identityPath, identity, client };
  };

  const withControlUiGatewayServer = async <T>(
    fn: (ctx: {
      port: number;
      server: Awaited<ReturnType<typeof startGatewayServer>>;
    }) => Promise<T>,
  ): Promise<T> => {
    return await withGatewayServer(fn, {
      serverOptions: { controlUiEnabled: true },
    });
  };

  const startControlUiServerWithClient = async (
    token?: string,
    opts?: Parameters<typeof startServerWithClient>[1],
  ) => {
    return await startServerWithClient(token, {
      ...opts,
      controlUiEnabled: true,
    });
  };

  const startControlUiServer = async (token?: string, opts?: Parameters<typeof startServer>[1]) => {
    return await startServer(token, {
      ...opts,
      controlUiEnabled: true,
    });
  };

  const getRequiredPairedMetadata = (
    paired: Record<string, Record<string, unknown>>,
    deviceId: string,
  ) => {
    const metadata = paired[deviceId];
    if (!metadata) {
      throw new Error(`Expected paired metadata for deviceId=${deviceId}`);
    }
    return metadata;
  };

  const stripPairedMetadataRolesAndScopes = async (deviceId: string) => {
    const { resolvePairingPaths, tryReadJson } = await import("../infra/pairing-files.js");
    const { writeJson } = await import("../infra/json-files.js");
    const { pairedPath } = resolvePairingPaths(undefined, "devices");
    const paired = (await tryReadJson<Record<string, Record<string, unknown>>>(pairedPath)) ?? {};
    const legacy = getRequiredPairedMetadata(paired, deviceId);
    delete legacy.roles;
    delete legacy.scopes;
    await writeJson(pairedPath, paired);
  };

  const overwritePairedPublicKey = async (deviceId: string, publicKey: string) => {
    const { resolvePairingPaths, tryReadJson } = await import("../infra/pairing-files.js");
    const { writeJson } = await import("../infra/json-files.js");
    const { pairedPath } = resolvePairingPaths(undefined, "devices");
    const paired = (await tryReadJson<Record<string, Record<string, unknown>>>(pairedPath)) ?? {};
    const metadata = getRequiredPairedMetadata(paired, deviceId);
    metadata.publicKey = publicKey;
    await writeJson(pairedPath, paired);
  };

  const seedApprovedOperatorReadPairing = async (params: {
    identityPrefix: string;
    clientId: string;
    clientMode: string;
    displayName: string;
    platform: string;
    scopes?: string[];
  }): Promise<{ identityPath: string; identity: { deviceId: string } }> => {
    const { publicKeyRawBase64UrlFromPem } = await import("../infra/device-identity.js");
    const { approveDevicePairing, requestDevicePairing } =
      await import("../infra/device-pairing.js");
    const { identityPath, identity } = await createOperatorIdentityFixture(params.identityPrefix);
    const scopes = params.scopes ?? ["operator.read"];
    const devicePublicKey = publicKeyRawBase64UrlFromPem(identity.publicKeyPem);
    const seeded = await requestDevicePairing({
      deviceId: identity.deviceId,
      publicKey: devicePublicKey,
      role: "operator",
      scopes,
      clientId: params.clientId,
      clientMode: params.clientMode,
      displayName: params.displayName,
      platform: params.platform,
    });
    await approveDevicePairing(seeded.request.requestId, {
      callerScopes: ["operator.admin"],
    });
    return { identityPath, identity: { deviceId: identity.deviceId } };
  };

  test("rejects untrusted trusted-proxy control ui device identity states", async () => {
    await configureTrustedProxyControlUiAuth();
    await withControlUiGatewayServer(async ({ port }) => {
      for (const tc of trustedProxyControlUiCases) {
        const ws = await openWs(port, TRUSTED_PROXY_CONTROL_UI_HEADERS);
        try {
          const scopes = tc.withUnpairedNodeDevice ? [] : undefined;
          let device: Awaited<ReturnType<typeof createSignedDevice>>["device"] | null = null;
          if (tc.withUnpairedNodeDevice) {
            const challengeNonce = await readConnectChallengeNonce(ws);
            if (!challengeNonce) {
              throw new Error(`expected connect challenge nonce for ${tc.name}`);
            }
            ({ device } = await createSignedDevice({
              token: null,
              role: "node",
              scopes: [],
              clientId: GATEWAY_CLIENT_NAMES.CONTROL_UI,
              clientMode: GATEWAY_CLIENT_MODES.WEBCHAT,
              nonce: challengeNonce,
            }));
          }
          const res = await connectReq(ws, {
            skipDefaultAuth: true,
            role: tc.role,
            scopes,
            device,
            client: { ...CONTROL_UI_CLIENT },
          });
          expect(res.ok, tc.name).toBe(tc.expectedOk);
          if (!tc.expectedOk) {
            if (tc.expectedErrorSubstring) {
              expect(res.error?.message ?? "", tc.name).toContain(tc.expectedErrorSubstring);
            }
            if (tc.expectedErrorCode) {
              expect((res.error?.details as { code?: string } | undefined)?.code, tc.name).toBe(
                tc.expectedErrorCode,
              );
            }
          }
        } finally {
          ws.close();
        }
      }
    });
  });

  test("rejects trusted-proxy control ui without device identity even with self-declared scopes", async () => {
    await configureTrustedProxyControlUiAuth();
    const { publicKeyRawBase64UrlFromPem } = await import("../infra/device-identity.js");
    const { rejectDevicePairing, requestDevicePairing } =
      await import("../infra/device-pairing.js");
    const { identity } = await createOperatorIdentityFixture("openclaw-control-ui-trusted-proxy-");
    const pendingRequest = await requestDevicePairing({
      deviceId: identity.deviceId,
      publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
      role: "operator",
      scopes: ["operator.admin"],
      clientId: CONTROL_UI_CLIENT.id,
      clientMode: CONTROL_UI_CLIENT.mode,
    });
    await withControlUiGatewayServer(async ({ port }) => {
      const ws = await openWs(port, TRUSTED_PROXY_CONTROL_UI_HEADERS);
      try {
        const res = await connectReq(ws, {
          skipDefaultAuth: true,
          scopes: ["operator.admin"],
          device: null,
          client: { ...CONTROL_UI_CLIENT },
        });
        expect(res.ok).toBe(false);
        expect(res.error?.message ?? "").toContain("control ui requires device identity");
        expect((res.error?.details as { code?: string } | undefined)?.code).toBe(
          ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
        );
      } finally {
        ws.close();
        await rejectDevicePairing(pendingRequest.request.requestId);
      }
    });
  });

  test("requires pairing for trusted-proxy control ui device identity", async () => {
    const { replaceConfigFile } = await import("../config/config.js");
    testState.gatewayAuth = undefined;
    testState.gatewayControlUi = {
      ...testState.gatewayControlUi,
      allowedOrigins: ["https://localhost"],
    };
    await replaceConfigFile({
      nextConfig: {
        gateway: {
          auth: {
            mode: "trusted-proxy",
            trustedProxy: {
              userHeader: "x-forwarded-user",
              requiredHeaders: ["x-forwarded-proto"],
              allowLoopback: true,
            },
          },
          trustedProxies: ["127.0.0.1"],
          controlUi: {
            allowedOrigins: ["https://localhost"],
          },
        },
      },
      afterWrite: { mode: "auto" },
    });
    await withControlUiGatewayServer(async ({ port }) => {
      const ws = await openWs(port, TRUSTED_PROXY_CONTROL_UI_HEADERS);
      try {
        const challengeNonce = await readConnectChallengeNonce(ws);
        const { device } = await createSignedDevice({
          token: null,
          role: "operator",
          scopes: ["operator.admin", "operator.read"],
          clientId: CONTROL_UI_CLIENT.id,
          clientMode: CONTROL_UI_CLIENT.mode,
          nonce: challengeNonce,
        });
        const res = await connectReq(ws, {
          skipDefaultAuth: true,
          scopes: ["operator.admin", "operator.read"],
          device,
          client: { ...CONTROL_UI_CLIENT },
        });
        expect(res.ok).toBe(false);
        expect(res.error?.message ?? "").toContain("pairing required");
        expect((res.error?.details as { code?: string } | undefined)?.code).toBe(
          ConnectErrorDetailCodes.PAIRING_REQUIRED,
        );
      } finally {
        ws.close();
      }
    });
  });

  test("clears trusted-proxy control ui scopes without device identity", async () => {
    const { replaceConfigFile } = await import("../config/config.js");
    testState.gatewayAuth = undefined;
    testState.gatewayControlUi = {
      ...testState.gatewayControlUi,
      allowedOrigins: ["https://localhost"],
    };
    await replaceConfigFile({
      nextConfig: {
        gateway: {
          auth: {
            mode: "trusted-proxy",
            trustedProxy: {
              userHeader: "x-forwarded-user",
              requiredHeaders: ["x-forwarded-proto"],
              allowLoopback: true,
            },
          },
          trustedProxies: ["127.0.0.1"],
          controlUi: {
            allowedOrigins: ["https://localhost"],
          },
        },
      },
      afterWrite: { mode: "auto" },
    });
    await withControlUiGatewayServer(async ({ port }) => {
      const ws = await openWs(port, TRUSTED_PROXY_CONTROL_UI_HEADERS);
      try {
        const res = await connectReq(ws, {
          skipDefaultAuth: true,
          scopes: ["operator.admin", "operator.read"],
          device: null,
          client: { ...CONTROL_UI_CLIENT },
        });
        expect(res.ok).toBe(true);
        const payload = res.payload as
          | {
              auth?: { scopes?: string[]; deviceToken?: string };
            }
          | undefined;
        expect(payload?.auth?.scopes).toEqual([]);
        expect(payload?.auth?.deviceToken).toBeUndefined();

        const admin = await rpcReq(ws, "set-heartbeats", { enabled: false });
        expect(admin.ok).toBe(false);
        expect(admin.error?.message ?? "").toContain("missing scope");
      } finally {
        ws.close();
      }
    });
  });

  test("bounds trusted-proxy control ui scopes to proxy-declared scope header", async () => {
    const { replaceConfigFile } = await import("../config/config.js");
    testState.gatewayAuth = undefined;
    testState.gatewayControlUi = {
      ...testState.gatewayControlUi,
      allowedOrigins: ["https://localhost"],
    };
    await replaceConfigFile({
      nextConfig: {
        gateway: {
          auth: {
            mode: "trusted-proxy",
            trustedProxy: {
              userHeader: "x-forwarded-user",
              requiredHeaders: ["x-forwarded-proto"],
              allowLoopback: true,
            },
          },
          trustedProxies: ["127.0.0.1"],
          controlUi: {
            allowedOrigins: ["https://localhost"],
          },
        },
      },
      afterWrite: { mode: "auto" },
    });
    await withControlUiGatewayServer(async ({ port }) => {
      const seeded = await seedApprovedOperatorReadPairing({
        identityPrefix: "openclaw-control-ui-trusted-proxy-bounded-",
        clientId: CONTROL_UI_CLIENT.id,
        clientMode: CONTROL_UI_CLIENT.mode,
        displayName: "Control UI",
        platform: "web",
        scopes: ["operator.admin", "operator.read"],
      });
      const ws = await openWs(port, {
        ...TRUSTED_PROXY_CONTROL_UI_HEADERS,
        "x-openclaw-scopes": "operator.read",
      });
      try {
        const challengeNonce = await readConnectChallengeNonce(ws);
        const { device } = await createSignedDevice({
          token: null,
          role: "operator",
          scopes: ["operator.admin", "operator.read"],
          clientId: CONTROL_UI_CLIENT.id,
          clientMode: CONTROL_UI_CLIENT.mode,
          identityPath: seeded.identityPath,
          nonce: challengeNonce,
        });
        const res = await connectReq(ws, {
          skipDefaultAuth: true,
          scopes: ["operator.admin", "operator.read"],
          device,
          client: { ...CONTROL_UI_CLIENT },
        });
        expect(res.ok).toBe(true);
        const payload = res.payload as
          | {
              auth?: { scopes?: string[]; deviceToken?: string };
            }
          | undefined;
        expect(payload?.auth?.scopes).toEqual(["operator.read"]);
        expect(payload?.auth?.deviceToken).toBeUndefined();

        const admin = await rpcReq(ws, "set-heartbeats", { enabled: false });
        expect(admin.ok).toBe(false);
        expect(admin.error?.message ?? "").toContain("missing scope");

        const health = await rpcReq(ws, "health");
        expect(health.ok).toBe(true);
      } finally {
        ws.close();
      }
    });
  });

  test("allows localhost ui clients without device identity when insecure auth is enabled", async () => {
    testState.gatewayControlUi = { allowInsecureAuth: true };
    const { server, ws, port, prevToken } = await startControlUiServerWithClient("secret", {
      wsHeaders: { origin: "http://127.0.0.1" },
    });
    let tuiWs: WebSocket | undefined;
    try {
      await connectControlUiWithoutDeviceAndExpectOk({ ws, token: "secret" });

      tuiWs = await openWs(port);
      await connectControlUiWithoutDeviceAndExpectOk({
        ws: tuiWs,
        token: "secret",
        client: {
          id: GATEWAY_CLIENT_NAMES.TUI,
          version: "1.0.0",
          platform: "darwin",
          mode: GATEWAY_CLIENT_MODES.UI,
        },
      });
    } finally {
      ws.close();
      tuiWs?.close();
      await Promise.all([
        waitForWsClose(ws, 1_000),
        ...(tuiWs ? [waitForWsClose(tuiWs, 1_000)] : []),
      ]);
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("allows control ui password-only auth on localhost when insecure auth is enabled", async () => {
    testState.gatewayControlUi = { allowInsecureAuth: true };
    testState.gatewayAuth = { mode: "password", password: "secret" }; // pragma: allowlist secret
    await withControlUiGatewayServer(async ({ port }) => {
      const ws = await openWs(port, { origin: originForPort(port) });
      await connectControlUiWithoutDeviceAndExpectOk({ ws, password: "secret" }); // pragma: allowlist secret
      ws.close();
    });
  });

  test("does not bypass pairing for control ui device identity when insecure auth is enabled", async () => {
    testState.gatewayControlUi = {
      allowInsecureAuth: true,
      allowedOrigins: ["https://localhost"],
    };
    testState.gatewayAuth = { mode: "token", token: "secret" };
    await writeTrustedProxyControlUiConfig({ allowInsecureAuth: true });
    const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
    process.env.OPENCLAW_GATEWAY_TOKEN = "secret";
    try {
      await withControlUiGatewayServer(async ({ port }) => {
        const ws = new WebSocket(`ws://127.0.0.1:${port}`, {
          headers: {
            origin: "https://localhost",
            "x-forwarded-for": "203.0.113.10",
          },
        });
        const challengePromise = onceMessage(
          ws,
          (o) => o.type === "event" && o.event === "connect.challenge",
        );
        await new Promise<void>((resolve) => ws.once("open", resolve));
        const challenge = await challengePromise;
        const nonce = (challenge.payload as { nonce?: unknown } | undefined)?.nonce;
        expect(typeof nonce).toBe("string");
        const { identityPath } = await createOperatorIdentityFixture("openclaw-controlui-device-");
        const scopes = [
          "operator.admin",
          "operator.read",
          "operator.write",
          "operator.approvals",
          "operator.pairing",
        ];
        const { device } = await createSignedDevice({
          token: "secret",
          scopes,
          clientId: GATEWAY_CLIENT_NAMES.CONTROL_UI,
          clientMode: GATEWAY_CLIENT_MODES.WEBCHAT,
          identityPath,
          nonce: String(nonce),
        });
        const res = await connectReq(ws, {
          token: "secret",
          scopes,
          device,
          client: {
            ...CONTROL_UI_CLIENT,
          },
        });
        expect(res.ok).toBe(false);
        expect(res.error?.message ?? "").toContain("pairing required");
        expect((res.error?.details as { code?: string } | undefined)?.code).toBe(
          ConnectErrorDetailCodes.PAIRING_REQUIRED,
        );
        ws.close();
      });
    } finally {
      restoreGatewayToken(prevToken);
    }
  });

  test("allows control ui auth bypasses when device auth is disabled", async () => {
    testState.gatewayControlUi = { dangerouslyDisableDeviceAuth: true };
    testState.gatewayAuth = { mode: "token", token: "secret" };
    const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
    process.env.OPENCLAW_GATEWAY_TOKEN = "secret";
    try {
      await withControlUiGatewayServer(async ({ port }) => {
        const staleDeviceWs = await openWs(port, { origin: originForPort(port) });
        const challengeNonce = await readConnectChallengeNonce(staleDeviceWs);
        if (!challengeNonce) {
          throw new Error("expected stale device challenge nonce");
        }
        const { device } = await createSignedDevice({
          token: "secret",
          scopes: [],
          clientId: GATEWAY_CLIENT_NAMES.CONTROL_UI,
          clientMode: GATEWAY_CLIENT_MODES.WEBCHAT,
          signedAtMs: Date.now() - 60 * 60 * 1000,
          nonce: challengeNonce,
        });
        const res = await connectReq(staleDeviceWs, {
          token: "secret",
          scopes: ["operator.read"],
          device,
          client: {
            ...CONTROL_UI_CLIENT,
          },
        });
        expect(res.ok).toBe(true);
        const helloOk = res.payload as
          | {
              auth?: {
                role?: unknown;
                scopes?: unknown;
                deviceToken?: unknown;
              };
            }
          | undefined;
        expect(helloOk?.auth?.role).toBe("operator");
        expect(helloOk?.auth?.scopes).toEqual(["operator.read"]);
        expect(helloOk?.auth?.deviceToken).toBeUndefined();
        const health = await rpcReq(staleDeviceWs, "health");
        expect(health.ok).toBe(true);
        staleDeviceWs.close();

        const scopedWs = await openWs(port, { origin: originForPort(port) });
        const scopedRes = await connectReq(scopedWs, {
          token: "secret",
          scopes: ["operator.read"],
          client: {
            ...CONTROL_UI_CLIENT,
          },
        });
        expect(scopedRes.ok, "requested scope bypass").toBe(true);
        const scopedHelloOk = scopedRes.payload as
          | {
              auth?: {
                role?: unknown;
                scopes?: unknown;
                deviceToken?: unknown;
              };
            }
          | undefined;
        expect(scopedHelloOk?.auth?.role).toBe("operator");
        expect(scopedHelloOk?.auth?.scopes).toEqual(["operator.read"]);
        expect(scopedHelloOk?.auth?.deviceToken).toBeUndefined();

        const scopedHealth = await rpcReq(scopedWs, "health");
        expect(scopedHealth.ok).toBe(true);
        scopedWs.close();
      });
    } finally {
      restoreGatewayToken(prevToken);
    }
  });

  test("device token auth matrix", async () => {
    const { server, ws, port, prevToken } = await startControlUiServerWithClient("secret");
    const { deviceToken, deviceIdentityPath } = await ensurePairedDeviceTokenForCurrentIdentity(ws);
    ws.close();

    const scenarios: Array<{
      name: string;
      opts: Parameters<typeof connectReq>[1];
      assert: (res: Awaited<ReturnType<typeof connectReq>>) => void;
    }> = [
      {
        name: "accepts device token auth for paired device",
        opts: { token: deviceToken },
        assert: (res) => {
          expect(res.ok).toBe(true);
        },
      },
      {
        name: "accepts explicit auth.deviceToken when shared token is omitted",
        opts: {
          skipDefaultAuth: true,
          deviceToken,
        },
        assert: (res) => {
          expect(res.ok).toBe(true);
        },
      },
      {
        name: "uses explicit auth.deviceToken fallback when shared token is wrong",
        opts: {
          token: "wrong",
          deviceToken,
        },
        assert: (res) => {
          expect(res.ok).toBe(true);
        },
      },
      {
        name: "keeps shared token mismatch reason when fallback device-token check fails",
        opts: { token: "wrong" },
        assert: (res) => {
          expect(res.ok).toBe(false);
          expect(res.error?.message ?? "").toContain("gateway token mismatch");
          expect(res.error?.message ?? "").not.toContain("device token mismatch");
          const details = res.error?.details as
            | {
                code?: string;
                canRetryWithDeviceToken?: boolean;
                recommendedNextStep?: string;
              }
            | undefined;
          expect(details?.code).toBe(ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH);
          expect(details?.canRetryWithDeviceToken).toBe(true);
          expect(details?.recommendedNextStep).toBe("retry_with_device_token");
        },
      },
      {
        name: "reports device token mismatch when explicit auth.deviceToken is wrong",
        opts: {
          skipDefaultAuth: true,
          deviceToken: "not-a-valid-device-token",
        },
        assert: (res) => {
          expect(res.ok).toBe(false);
          expect(res.error?.message ?? "").toContain("device token mismatch");
          expect((res.error?.details as { code?: string } | undefined)?.code).toBe(
            ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
          );
        },
      },
    ];

    try {
      for (const scenario of scenarios) {
        const ws2 = await openWs(port);
        try {
          const res = await connectReq(ws2, {
            ...scenario.opts,
            deviceIdentityPath,
          });
          scenario.assert(res);
        } finally {
          ws2.close();
        }
      }
    } finally {
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("keeps shared-secret lockout separate from device-token auth", async () => {
    const { server, port, prevToken, deviceToken, deviceIdentityPath } =
      await startRateLimitedTokenServerWithPairedDeviceToken();
    try {
      const wsBadShared = await openWs(port);
      const badShared = await connectReq(wsBadShared, { token: "wrong", device: null });
      expect(badShared.ok).toBe(false);
      wsBadShared.close();

      const wsSharedLocked = await openWs(port);
      const sharedLocked = await connectReq(wsSharedLocked, { token: "secret", device: null });
      expect(sharedLocked.ok).toBe(false);
      expect(sharedLocked.error?.message ?? "").toContain("retry later");
      wsSharedLocked.close();

      const wsDevice = await openWs(port);
      const deviceOk = await connectReq(wsDevice, { token: deviceToken, deviceIdentityPath });
      expect(deviceOk.ok).toBe(true);
      wsDevice.close();
    } finally {
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("keeps device-token lockout separate from shared-secret auth", async () => {
    const { server, port, prevToken, deviceToken, deviceIdentityPath } =
      await startRateLimitedTokenServerWithPairedDeviceToken();
    try {
      const wsBadDevice = await openWs(port);
      const badDevice = await connectReq(wsBadDevice, {
        skipDefaultAuth: true,
        deviceToken: "wrong",
        deviceIdentityPath,
      });
      expect(badDevice.ok).toBe(false);
      wsBadDevice.close();

      const wsDeviceLocked = await openWs(port);
      const deviceLocked = await connectReq(wsDeviceLocked, {
        skipDefaultAuth: true,
        deviceToken: "wrong",
        deviceIdentityPath,
      });
      expect(deviceLocked.ok).toBe(false);
      expect(deviceLocked.error?.message ?? "").toContain("retry later");
      wsDeviceLocked.close();

      const wsShared = await openWs(port);
      const sharedOk = await connectReq(wsShared, { token: "secret", device: null });
      expect(sharedOk.ok).toBe(true);
      wsShared.close();

      const wsDeviceReal = await openWs(port);
      const deviceStillLocked = await connectReq(wsDeviceReal, {
        token: deviceToken,
        deviceIdentityPath,
      });
      expect(deviceStillLocked.ok).toBe(false);
      expect(deviceStillLocked.error?.message ?? "").toContain("retry later");
      wsDeviceReal.close();
    } finally {
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("auto-approves local-direct operator pairing despite a remote-looking host header", async () => {
    const { getPairedDevice, listDevicePairing } = await import("../infra/device-pairing.js");
    const { server, port, prevToken, identityPath, identity, client } =
      await startControlUiServerWithOperatorIdentity();

    const wsRemoteRead = await openWs(port, { host: "gateway.example" });
    const initialNonce = await readConnectChallengeNonce(wsRemoteRead);
    const initial = await connectReq(wsRemoteRead, {
      token: "secret",
      scopes: ["operator.read"],
      client,
      device: await buildSignedDeviceForIdentity({
        identityPath,
        client,
        scopes: ["operator.read"],
        nonce: initialNonce,
      }),
    });
    expect(initial.ok).toBe(true);
    let pairing = await listDevicePairing();
    const pendingAfterRead = pairing.pending.filter(
      (entry) => entry.deviceId === identity.deviceId,
    );
    expect(pendingAfterRead).toHaveLength(0);
    if (!(await getPairedDevice(identity.deviceId))) {
      throw new Error(`expected paired device ${identity.deviceId}`);
    }
    wsRemoteRead.close();

    const ws2 = await openWs(port, { host: "gateway.example" });
    const nonce2 = await readConnectChallengeNonce(ws2);
    const res = await connectReq(ws2, {
      token: "secret",
      scopes: ["operator.admin"],
      client,
      device: await buildSignedDeviceForIdentity({
        identityPath,
        client,
        scopes: ["operator.admin"],
        nonce: nonce2,
      }),
    });
    expect(res.ok).toBe(false);
    expect(res.error?.message ?? "").toContain("pairing required");
    pairing = await listDevicePairing();
    const pendingAfterAdmin = pairing.pending.filter(
      (entry) => entry.deviceId === identity.deviceId,
    );
    expect(pendingAfterAdmin).toHaveLength(1);
    expectArrayIncludes(pendingAfterAdmin[0]?.scopes, ["operator.admin"]);
    if (!(await getPairedDevice(identity.deviceId))) {
      throw new Error(`expected paired device ${identity.deviceId}`);
    }
    ws2.close();
    await server.close();
    restoreGatewayToken(prevToken);
  });

  test("requires approval for loopback scope upgrades for control ui clients", async () => {
    const { getPairedDevice, listDevicePairing } = await import("../infra/device-pairing.js");
    const { server, port, prevToken } = await startControlUiServer("secret");
    const { identity, identityPath } = await seedApprovedOperatorReadPairing({
      identityPrefix: "openclaw-device-token-scope-",
      clientId: CONTROL_UI_CLIENT.id,
      clientMode: CONTROL_UI_CLIENT.mode,
      displayName: "loopback-control-ui-upgrade",
      platform: CONTROL_UI_CLIENT.platform,
    });

    const ws2 = await openWs(port, { origin: originForPort(port) });
    const nonce2 = await readConnectChallengeNonce(ws2);
    const upgraded = await connectReq(ws2, {
      token: "secret",
      scopes: ["operator.admin"],
      client: { ...CONTROL_UI_CLIENT },
      device: await buildSignedDeviceForIdentity({
        identityPath,
        client: CONTROL_UI_CLIENT,
        scopes: ["operator.admin"],
        nonce: nonce2,
      }),
    });
    expect(upgraded.ok).toBe(false);
    expect(upgraded.error?.message ?? "").toContain("pairing required");
    const pending = await listDevicePairing();
    const pendingUpgrade = pending.pending.filter((entry) => entry.deviceId === identity.deviceId);
    expect(pendingUpgrade).toHaveLength(1);
    expectArrayIncludes(pendingUpgrade[0]?.scopes, ["operator.admin"]);
    const updated = await getPairedDevice(identity.deviceId);
    expect(updated?.tokens?.operator?.scopes ?? []).not.toContain("operator.admin");

    ws2.close();
    await server.close();
    restoreGatewayToken(prevToken);
  });

  test("does not expose approved access when a paired device id reconnects with a different key", async () => {
    const { identity, identityPath } = await seedApprovedOperatorReadPairing({
      identityPrefix: "openclaw-device-key-mismatch-",
      clientId: TEST_OPERATOR_CLIENT.id,
      clientMode: TEST_OPERATOR_CLIENT.mode,
      displayName: "remote-key-mismatch",
      platform: TEST_OPERATOR_CLIENT.platform,
    });
    await overwritePairedPublicKey(identity.deviceId, "mismatched-public-key");

    const { server, port, prevToken } = await startControlUiServer("secret");
    const ws2 = await openTailscaleWs(port);
    try {
      const nonce2 = await readConnectChallengeNonce(ws2);
      const mismatched = await connectReq(ws2, {
        token: "secret",
        scopes: ["operator.admin"],
        client: { ...TEST_OPERATOR_CLIENT },
        device: await buildSignedDeviceForIdentity({
          identityPath,
          client: TEST_OPERATOR_CLIENT,
          scopes: ["operator.admin"],
          nonce: nonce2,
        }),
      });
      expect(mismatched.ok).toBe(false);
      expect(mismatched.error?.message ?? "").toContain("pairing required");
      expect(
        (
          mismatched.error?.details as
            | {
                reason?: string;
                requestedRole?: string;
                requestedScopes?: string[];
                approvedRoles?: string[];
                approvedScopes?: string[];
              }
            | undefined
        )?.reason,
      ).toBe("not-paired");
      expect(
        (
          mismatched.error?.details as
            | {
                requestedRole?: string;
                requestedScopes?: string[];
              }
            | undefined
        )?.requestedRole,
      ).toBe("operator");
      expect(
        (
          mismatched.error?.details as
            | {
                requestedRole?: string;
                requestedScopes?: string[];
              }
            | undefined
        )?.requestedScopes,
      ).toEqual(["operator.admin"]);
      expect(
        (
          mismatched.error?.details as
            | {
                approvedRoles?: string[];
                approvedScopes?: string[];
              }
            | undefined
        )?.approvedRoles,
      ).toBeUndefined();
      expect(
        (
          mismatched.error?.details as
            | {
                approvedRoles?: string[];
                approvedScopes?: string[];
              }
            | undefined
        )?.approvedScopes,
      ).toBeUndefined();
    } finally {
      ws2.close();
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("qr setup code returns node token plus bounded operator handoff", async () => {
    const { issueDeviceBootstrapToken, verifyDeviceBootstrapToken } =
      await import("../infra/device-bootstrap.js");
    const { publicKeyRawBase64UrlFromPem } = await import("../infra/device-identity.js");
    const { getPairedDevice, listDevicePairing, verifyDeviceToken } =
      await import("../infra/device-pairing.js");
    const { server, port, prevToken } = await startControlUiServer("secret");

    const { identityPath, identity } = await createOperatorIdentityFixture(
      "openclaw-bootstrap-node-",
    );
    const client = {
      id: "openclaw-ios",
      version: "2026.3.30",
      platform: "iOS 26.3.1",
      mode: "node",
      deviceFamily: "iPhone",
    };

    try {
      const issued = await issueDeviceBootstrapToken();
      const wsBootstrap = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      const initial = await connectReq(wsBootstrap, {
        skipDefaultAuth: true,
        bootstrapToken: issued.token,
        role: "node",
        scopes: [],
        client,
        deviceIdentityPath: identityPath,
      });
      expect(initial.ok).toBe(true);
      const approvedPayload = initial.payload as
        | {
            type?: string;
            auth?: {
              deviceToken?: string;
              role?: string;
              scopes?: string[];
              deviceTokens?: Array<{
                deviceToken?: string;
                role?: string;
                scopes?: string[];
              }>;
            };
          }
        | undefined;
      expect(approvedPayload?.type).toBe("hello-ok");
      const issuedDeviceToken = approvedPayload?.auth?.deviceToken;
      if (!issuedDeviceToken) {
        throw new Error("expected issued device token");
      }
      expect(approvedPayload?.auth?.role).toBe("node");
      expect(approvedPayload?.auth?.scopes ?? []).toEqual([]);
      const operatorHandoff = approvedPayload?.auth?.deviceTokens?.find(
        (entry) => entry.role === "operator",
      );
      const issuedOperatorToken = operatorHandoff?.deviceToken;
      if (!issuedOperatorToken) {
        throw new Error("expected handed-off operator device token");
      }
      expect(operatorHandoff?.scopes).toEqual([
        "operator.approvals",
        "operator.read",
        "operator.talk.secrets",
        "operator.write",
      ]);
      expect(operatorHandoff?.scopes).not.toContain("operator.admin");
      expect(operatorHandoff?.scopes).not.toContain("operator.pairing");

      const pendingAfterInitial = await listDevicePairing();
      const pendingForDevice = pendingAfterInitial.pending.filter(
        (entry) => entry.deviceId === identity.deviceId,
      );
      expect(pendingForDevice).toEqual([]);
      wsBootstrap.close();

      const afterBootstrap = await listDevicePairing();
      expect(
        afterBootstrap.pending.filter((entry) => entry.deviceId === identity.deviceId),
      ).toEqual([]);
      const paired = await getPairedDevice(identity.deviceId);
      expect(paired?.roles).toEqual(["node", "operator"]);
      expect(paired?.approvedScopes).toEqual([
        "operator.approvals",
        "operator.read",
        "operator.talk.secrets",
        "operator.write",
      ]);
      expect(paired?.tokens?.node?.token).toBe(issuedDeviceToken);
      expect(paired?.tokens?.node?.scopes).toEqual([]);
      expect(paired?.tokens?.operator?.token).toBe(issuedOperatorToken);
      expect(paired?.tokens?.operator?.scopes).toEqual([
        "operator.approvals",
        "operator.read",
        "operator.talk.secrets",
        "operator.write",
      ]);

      const wsReplay = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      const replay = await connectReq(wsReplay, {
        skipDefaultAuth: true,
        bootstrapToken: issued.token,
        role: "node",
        scopes: [],
        client,
        deviceIdentityPath: identityPath,
      });
      expect(replay.ok).toBe(false);
      expect((replay.error?.details as { code?: string } | undefined)?.code).toBe(
        ConnectErrorDetailCodes.AUTH_BOOTSTRAP_TOKEN_INVALID,
      );
      wsReplay.close();

      const wsReconnect = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      const reconnect = await connectReq(wsReconnect, {
        skipDefaultAuth: true,
        deviceToken: issuedDeviceToken,
        role: "node",
        scopes: [],
        client,
        deviceIdentityPath: identityPath,
      });
      expect(reconnect.ok).toBe(true);
      wsReconnect.close();

      await expect(
        verifyDeviceBootstrapToken({
          token: issued.token,
          deviceId: identity.deviceId,
          publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
          role: "node",
          scopes: [],
        }),
      ).resolves.toEqual({ ok: false, reason: "bootstrap_token_invalid" });

      await expect(
        verifyDeviceToken({
          deviceId: identity.deviceId,
          token: issuedDeviceToken,
          role: "node",
          scopes: [],
        }),
      ).resolves.toEqual({ ok: true });
      await expect(
        verifyDeviceToken({
          deviceId: identity.deviceId,
          token: issuedOperatorToken,
          role: "operator",
          scopes: [
            "operator.approvals",
            "operator.read",
            "operator.talk.secrets",
            "operator.write",
          ],
        }),
      ).resolves.toEqual({ ok: true });
      await expect(
        verifyDeviceToken({
          deviceId: identity.deviceId,
          token: issuedOperatorToken,
          role: "operator",
          scopes: ["operator.admin"],
        }),
      ).resolves.toEqual({ ok: false, reason: "scope-mismatch" });
      await expect(
        verifyDeviceToken({
          deviceId: identity.deviceId,
          token: issuedOperatorToken,
          role: "operator",
          scopes: ["operator.pairing"],
        }),
      ).resolves.toEqual({ ok: false, reason: "scope-mismatch" });
    } finally {
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("qr bootstrap retry keeps bounded operator handoff after paired approval", async () => {
    const { issueDeviceBootstrapToken, verifyDeviceBootstrapToken } =
      await import("../infra/device-bootstrap.js");
    const { publicKeyRawBase64UrlFromPem } = await import("../infra/device-identity.js");
    const { approveBootstrapDevicePairing, requestDevicePairing } =
      await import("../infra/device-pairing.js");
    const { PAIRING_SETUP_BOOTSTRAP_PROFILE } =
      await import("../shared/device-bootstrap-profile.js");
    const { server, port, prevToken } = await startControlUiServer("secret");
    const { identityPath, identity } = await createOperatorIdentityFixture(
      "openclaw-bootstrap-node-retry-",
    );
    const client = {
      id: "openclaw-ios",
      version: "2026.3.30",
      platform: "iOS 26.3.1",
      mode: "node",
      deviceFamily: "iPhone",
    };

    try {
      const issued = await issueDeviceBootstrapToken();
      const publicKey = publicKeyRawBase64UrlFromPem(identity.publicKeyPem);
      const pending = await requestDevicePairing({
        deviceId: identity.deviceId,
        publicKey,
        role: "node",
        roles: ["node", "operator"],
        scopes: ["operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"],
        clientId: client.id,
        clientMode: client.mode,
        displayName: client.id,
        platform: client.platform,
        deviceFamily: client.deviceFamily,
        silent: true,
      });
      await approveBootstrapDevicePairing(
        pending.request.requestId,
        PAIRING_SETUP_BOOTSTRAP_PROFILE,
      );

      const wsRetry = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      const retry = await connectReq(wsRetry, {
        skipDefaultAuth: true,
        bootstrapToken: issued.token,
        role: "node",
        scopes: [],
        client,
        deviceIdentityPath: identityPath,
      });
      expect(retry.ok).toBe(true);
      const payload = retry.payload as
        | {
            auth?: {
              deviceToken?: string;
              deviceTokens?: Array<{ deviceToken?: string; role?: string; scopes?: string[] }>;
            };
          }
        | undefined;
      expect(payload?.auth?.deviceToken).toBeTruthy();
      const operatorHandoff = payload?.auth?.deviceTokens?.find(
        (entry) => entry.role === "operator",
      );
      expect(operatorHandoff?.deviceToken).toBeTruthy();
      expect(operatorHandoff?.scopes).toEqual([
        "operator.approvals",
        "operator.read",
        "operator.talk.secrets",
        "operator.write",
      ]);
      expect(operatorHandoff?.scopes).not.toContain("operator.admin");
      expect(operatorHandoff?.scopes).not.toContain("operator.pairing");
      wsRetry.close();

      await expect(
        verifyDeviceBootstrapToken({
          token: issued.token,
          deviceId: identity.deviceId,
          publicKey,
          role: "node",
          scopes: [],
        }),
      ).resolves.toEqual({ ok: false, reason: "bootstrap_token_invalid" });
    } finally {
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("rejected non-baseline bootstrap request cannot recreate pending node pairing", async () => {
    const { issueDeviceBootstrapToken } = await import("../infra/device-bootstrap.js");
    const { listDevicePairing, rejectDevicePairing } = await import("../infra/device-pairing.js");
    const { server, port, prevToken } = await startControlUiServer("secret");
    const { identityPath, identity } = await createOperatorIdentityFixture(
      "openclaw-bootstrap-node-reject-",
    );
    const client = {
      id: "openclaw-ios",
      version: "2026.3.30",
      platform: "iOS 26.3.1",
      mode: "node",
      deviceFamily: "iPhone",
    };

    try {
      const issued = await issueDeviceBootstrapToken({
        profile: {
          roles: ["node"],
          scopes: [],
        },
      });
      const wsInitial = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      const initial = await connectReq(wsInitial, {
        skipDefaultAuth: true,
        bootstrapToken: issued.token,
        role: "node",
        scopes: [],
        client,
        deviceIdentityPath: identityPath,
      });
      expect(initial.ok).toBe(false);
      expect(
        initial.error?.details as { code?: string; pauseReconnect?: boolean } | undefined,
      ).toMatchObject({
        code: ConnectErrorDetailCodes.PAIRING_REQUIRED,
        pauseReconnect: false,
      });
      wsInitial.close();

      const pending = (await listDevicePairing()).pending.find(
        (entry) => entry.deviceId === identity.deviceId,
      );
      if (!pending) {
        throw new Error("expected pending bootstrap pairing request");
      }
      await expect(rejectDevicePairing(pending.requestId)).resolves.toEqual({
        requestId: pending.requestId,
        deviceId: identity.deviceId,
      });

      const wsRetry = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      const retry = await connectReq(wsRetry, {
        skipDefaultAuth: true,
        bootstrapToken: issued.token,
        role: "node",
        scopes: [],
        client,
        deviceIdentityPath: identityPath,
      });
      expect(retry.ok).toBe(false);
      expect((retry.error?.details as { code?: string } | undefined)?.code).toBe(
        ConnectErrorDetailCodes.AUTH_BOOTSTRAP_TOKEN_INVALID,
      );
      wsRetry.close();
      expect(
        (await listDevicePairing()).pending.filter((entry) => entry.deviceId === identity.deviceId),
      ).toEqual([]);
    } finally {
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("does not consume bootstrap token when node reconcile fails before hello-ok", async () => {
    const { issueDeviceBootstrapToken } = await import("../infra/device-bootstrap.js");
    const { approveDevicePairing, listDevicePairing } = await import("../infra/device-pairing.js");
    const reconcileModule = await import("./node-connect-reconcile.js");
    const reconcileSpy = vi
      .spyOn(reconcileModule, "reconcileNodePairingOnConnect")
      .mockRejectedValueOnce(new Error("boom"));
    const { server, port, prevToken } = await startControlUiServer("secret");

    const { identityPath, client } = await createOperatorIdentityFixture(
      "openclaw-bootstrap-reconcile-fail-",
    );
    const nodeClient = {
      ...client,
      id: "openclaw-android",
      mode: "node",
    };

    try {
      const issued = await issueDeviceBootstrapToken({
        profile: {
          roles: ["node"],
          scopes: [],
        },
      });

      const wsInitial = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      const initial = await connectReq(wsInitial, {
        skipDefaultAuth: true,
        bootstrapToken: issued.token,
        role: "node",
        scopes: [],
        client: nodeClient,
        deviceIdentityPath: identityPath,
      });
      expect(initial.ok).toBe(false);
      wsInitial.close();
      const pending = (await listDevicePairing()).pending.find(
        (entry) => entry.clientId === nodeClient.id,
      );
      if (!pending) {
        throw new Error("expected pending bootstrap pairing request");
      }
      await approveDevicePairing(pending.requestId, { callerScopes: ["operator.pairing"] });

      const wsFail = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      await expect(
        connectReq(wsFail, {
          skipDefaultAuth: true,
          bootstrapToken: issued.token,
          role: "node",
          scopes: [],
          client: nodeClient,
          deviceIdentityPath: identityPath,
          timeoutMs: 500,
        }),
      ).rejects.toThrow();
      // The full agentic shard can saturate the event loop enough that the
      // server-side close after a pre-hello failure arrives later than 1s.
      await expect(waitForWsClose(wsFail, 5_000)).resolves.toBe(true);

      const wsRetry = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      const retry = await connectReq(wsRetry, {
        skipDefaultAuth: true,
        bootstrapToken: issued.token,
        role: "node",
        scopes: [],
        client: nodeClient,
        deviceIdentityPath: identityPath,
      });
      expect(retry.ok).toBe(true);
      wsRetry.close();
    } finally {
      reconcileSpy.mockRestore();
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("requires approval for bootstrap-auth role upgrades on already-paired devices", async () => {
    const { issueDeviceBootstrapToken } = await import("../infra/device-bootstrap.js");
    const { approveDevicePairing, getPairedDevice, listDevicePairing, requestDevicePairing } =
      await import("../infra/device-pairing.js");
    const { publicKeyRawBase64UrlFromPem } = await import("../infra/device-identity.js");
    const { server, port, prevToken } = await startControlUiServer("secret");

    const { identityPath, identity } = await createOperatorIdentityFixture(
      "openclaw-bootstrap-role-upgrade-",
    );
    const client = {
      id: "openclaw-ios",
      version: "2026.3.30",
      platform: "iOS 26.3.1",
      mode: "node",
      deviceFamily: "iPhone",
    };

    try {
      const seededRequest = await requestDevicePairing({
        deviceId: identity.deviceId,
        publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
        role: "operator",
        scopes: ["operator.read"],
        clientId: client.id,
        clientMode: client.mode,
        platform: client.platform,
        deviceFamily: client.deviceFamily,
      });
      await approveDevicePairing(seededRequest.request.requestId, {
        callerScopes: ["operator.read"],
      });

      const issued = await issueDeviceBootstrapToken({
        profile: {
          roles: ["node"],
          scopes: [],
        },
      });
      const wsUpgrade = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      const upgrade = await connectReq(wsUpgrade, {
        skipDefaultAuth: true,
        bootstrapToken: issued.token,
        role: "node",
        scopes: [],
        client,
        deviceIdentityPath: identityPath,
      });
      expect(upgrade.ok).toBe(false);
      expect(upgrade.error?.message ?? "").toContain("pairing required");
      expect((upgrade.error?.details as { code?: string; reason?: string } | undefined)?.code).toBe(
        ConnectErrorDetailCodes.PAIRING_REQUIRED,
      );
      expect(
        (upgrade.error?.details as { code?: string; reason?: string } | undefined)?.reason,
      ).toBe("role-upgrade");
      expect(
        (
          upgrade.error?.details as
            | {
                requestedRole?: string;
                approvedRoles?: string[];
              }
            | undefined
        )?.requestedRole,
      ).toBe("node");
      expect(
        (
          upgrade.error?.details as
            | {
                requestedRole?: string;
                approvedRoles?: string[];
              }
            | undefined
        )?.approvedRoles,
      ).toEqual(["operator"]);

      const pending = (await listDevicePairing()).pending.filter(
        (entry) => entry.deviceId === identity.deviceId,
      );
      expect(pending).toHaveLength(1);
      expect(pending[0]?.role).toBe("node");
      expect(pending[0]?.roles).toEqual(["node"]);
      const paired = await getPairedDevice(identity.deviceId);
      expectArrayIncludes(paired?.roles, ["operator"]);
      wsUpgrade.close();
    } finally {
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("requires approval for bootstrap-auth operator pairing outside the qr baseline profile", async () => {
    const { issueDeviceBootstrapToken } = await import("../infra/device-bootstrap.js");
    const { getPairedDevice, listDevicePairing } = await import("../infra/device-pairing.js");
    const { server, port, prevToken } = await startControlUiServer("secret");

    const { identityPath, identity, client } = await createOperatorIdentityFixture(
      "openclaw-bootstrap-operator-",
    );

    try {
      const issued = await issueDeviceBootstrapToken({
        profile: {
          roles: ["operator"],
          scopes: ["operator.read"],
        },
      });
      const wsBootstrap = await openWs(port, REMOTE_BOOTSTRAP_HEADERS);
      const initial = await connectReq(wsBootstrap, {
        skipDefaultAuth: true,
        bootstrapToken: issued.token,
        role: "operator",
        scopes: ["operator.read"],
        client,
        deviceIdentityPath: identityPath,
      });
      expect(initial.ok).toBe(false);
      expect(initial.error?.message ?? "").toContain("pairing required");
      expect((initial.error?.details as { code?: string } | undefined)?.code).toBe(
        ConnectErrorDetailCodes.PAIRING_REQUIRED,
      );

      const pending = (await listDevicePairing()).pending.filter(
        (entry) => entry.deviceId === identity.deviceId,
      );
      expect(pending).toHaveLength(1);
      expect(pending[0]?.role).toBe("operator");
      expectArrayIncludes(pending[0]?.scopes, ["operator.read"]);
      expect(await getPairedDevice(identity.deviceId)).toBeNull();
      wsBootstrap.close();
    } finally {
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("auto-approves local-direct node pairing, then queues operator scope approval", async () => {
    const { getPairedDevice, listDevicePairing } = await import("../infra/device-pairing.js");
    const { server, port, prevToken } = await startControlUiServer("secret");
    const { identityPath, identity, client } =
      await createOperatorIdentityFixture("openclaw-device-scope-");
    const connectWithNonce = async (role: "operator" | "node", scopes: string[]) => {
      const socket = new WebSocket(`ws://127.0.0.1:${port}`, {
        headers: { host: "gateway.example" },
      });
      const challengePromise = onceMessage(
        socket,
        (o) => o.type === "event" && o.event === "connect.challenge",
      );
      await new Promise<void>((resolve) => socket.once("open", resolve));
      const challenge = await challengePromise;
      const nonce = (challenge.payload as { nonce?: unknown } | undefined)?.nonce;
      expect(typeof nonce).toBe("string");
      const result = await connectReq(socket, {
        token: "secret",
        role,
        scopes,
        client,
        device: await buildSignedDeviceForIdentity({
          identityPath,
          client,
          role,
          scopes,
          nonce: String(nonce),
        }),
      });
      socket.close();
      return result;
    };

    const nodeConnect = await connectWithNonce("node", []);
    expect(nodeConnect.ok).toBe(true);

    const operatorConnect = await connectWithNonce("operator", ["operator.read", "operator.write"]);
    expect(operatorConnect.ok).toBe(false);
    expect(operatorConnect.error?.message ?? "").toContain("pairing required");

    const pending = await listDevicePairing();
    const pendingForTestDevice = pending.pending.filter(
      (entry) => entry.deviceId === identity.deviceId,
    );
    expect(pendingForTestDevice).toHaveLength(1);
    expectArrayIncludes(pendingForTestDevice[0]?.scopes, ["operator.read", "operator.write"]);

    const paired = await getPairedDevice(identity.deviceId);
    expectArrayIncludes(paired?.roles, ["node", "operator"]);
    expectArrayIncludes(paired?.approvedScopes, ["operator.read", "operator.write"]);

    const approvedOperatorConnect = await connectWithNonce("operator", ["operator.read"]);
    expect(approvedOperatorConnect.ok).toBe(true);

    await server.close();
    restoreGatewayToken(prevToken);
  });

  test("allows operator.read connect when device is paired with operator.admin", async () => {
    const { listDevicePairing } = await import("../infra/device-pairing.js");
    const { identityPath, identity } = await seedApprovedOperatorReadPairing({
      identityPrefix: "openclaw-device-admin-superset-",
      clientId: TEST_OPERATOR_CLIENT.id,
      clientMode: TEST_OPERATOR_CLIENT.mode,
      displayName: "operator-admin-superset",
      platform: TEST_OPERATOR_CLIENT.platform,
      scopes: ["operator.admin"],
    });

    const { server, port, prevToken } = await startControlUiServer("secret");

    const ws2 = await openWs(port);
    const nonce2 = await readConnectChallengeNonce(ws2);
    const res = await connectReq(ws2, {
      token: "secret",
      scopes: ["operator.read"],
      client: TEST_OPERATOR_CLIENT,
      device: await buildSignedDeviceForIdentity({
        identityPath,
        client: TEST_OPERATOR_CLIENT,
        scopes: ["operator.read"],
        nonce: nonce2,
      }),
    });
    expect(res.ok).toBe(true);
    ws2.close();

    const list = await listDevicePairing();
    expect(list.pending.filter((entry) => entry.deviceId === identity.deviceId)).toEqual([]);

    await server.close();
    restoreGatewayToken(prevToken);
  });

  test("allows operator shared auth with legacy paired metadata", async () => {
    const { publicKeyRawBase64UrlFromPem } = await import("../infra/device-identity.js");
    const { approveDevicePairing, getPairedDevice, listDevicePairing, requestDevicePairing } =
      await import("../infra/device-pairing.js");
    const { identityPath, identity } = await createOperatorIdentityFixture(
      "openclaw-device-legacy-meta-",
    );
    const deviceId = identity.deviceId;
    const publicKey = publicKeyRawBase64UrlFromPem(identity.publicKeyPem);
    const pending = await requestDevicePairing({
      deviceId,
      publicKey,
      role: "operator",
      scopes: ["operator.read"],
      clientId: TEST_OPERATOR_CLIENT.id,
      clientMode: TEST_OPERATOR_CLIENT.mode,
      displayName: "legacy-test",
      platform: "test",
    });
    await approveDevicePairing(pending.request.requestId, {
      callerScopes: pending.request.scopes ?? ["operator.admin"],
    });

    await stripPairedMetadataRolesAndScopes(deviceId);

    const { server, port, prevToken } = await startControlUiServer("secret");
    let ws2: WebSocket | undefined;
    try {
      const wsReconnect = await openWs(port);
      ws2 = wsReconnect;
      const reconnectNonce = await readConnectChallengeNonce(wsReconnect);
      const reconnect = await connectReq(wsReconnect, {
        token: "secret",
        scopes: ["operator.read"],
        client: TEST_OPERATOR_CLIENT,
        device: await buildSignedDeviceForIdentity({
          identityPath,
          client: TEST_OPERATOR_CLIENT,
          scopes: ["operator.read"],
          nonce: reconnectNonce,
        }),
      });
      expect(reconnect.ok).toBe(true);

      const repaired = await getPairedDevice(deviceId);
      expect(repaired?.role).toBe("operator");
      expect(repaired?.approvedScopes ?? []).toContain("operator.read");
      expect(repaired?.tokens?.operator?.scopes ?? []).toContain("operator.read");
      const list = await listDevicePairing();
      expect(list.pending.filter((entry) => entry.deviceId === deviceId)).toEqual([]);
    } finally {
      await server.close();
      restoreGatewayToken(prevToken);
      ws2?.close();
    }
  });

  test("requires approval for local scope upgrades even when paired metadata is legacy-shaped", async () => {
    const { getPairedDevice, listDevicePairing } = await import("../infra/device-pairing.js");
    const { identity, identityPath } = await seedApprovedOperatorReadPairing({
      identityPrefix: "openclaw-device-legacy-",
      clientId: TEST_OPERATOR_CLIENT.id,
      clientMode: TEST_OPERATOR_CLIENT.mode,
      displayName: "legacy-upgrade-test",
      platform: "test",
    });

    await stripPairedMetadataRolesAndScopes(identity.deviceId);

    const { server, port, prevToken } = await startControlUiServer("secret");
    let ws2: WebSocket | undefined;
    try {
      const client = { ...TEST_OPERATOR_CLIENT };

      const wsUpgrade = await openWs(port);
      ws2 = wsUpgrade;
      const upgradeNonce = await readConnectChallengeNonce(wsUpgrade);
      const upgraded = await connectReq(wsUpgrade, {
        token: "secret",
        scopes: ["operator.admin"],
        client,
        device: await buildSignedDeviceForIdentity({
          identityPath,
          client,
          scopes: ["operator.admin"],
          nonce: upgradeNonce,
        }),
      });
      expect(upgraded.ok).toBe(false);
      expect(upgraded.error?.message ?? "").toContain("pairing required");
      expect(
        (
          upgraded.error?.details as
            | {
                reason?: string;
                requestedRole?: string;
                requestedScopes?: string[];
                approvedScopes?: string[];
              }
            | undefined
        )?.reason,
      ).toBe("scope-upgrade");
      expect(
        (
          upgraded.error?.details as
            | {
                reason?: string;
                requestedRole?: string;
                requestedScopes?: string[];
                approvedScopes?: string[];
              }
            | undefined
        )?.requestedRole,
      ).toBe("operator");
      expect(
        (
          upgraded.error?.details as
            | {
                reason?: string;
                requestedRole?: string;
                requestedScopes?: string[];
                approvedScopes?: string[];
              }
            | undefined
        )?.requestedScopes,
      ).toEqual(["operator.admin"]);
      expect(
        (
          upgraded.error?.details as
            | {
                reason?: string;
                requestedRole?: string;
                requestedScopes?: string[];
                approvedScopes?: string[];
              }
            | undefined
        )?.approvedScopes,
      ).toEqual(["operator.read"]);
      wsUpgrade.close();

      const pendingUpgrade = (await listDevicePairing()).pending.find(
        (entry) => entry.deviceId === identity.deviceId,
      );
      if (!pendingUpgrade) {
        throw new Error(`expected pending upgrade for device ${identity.deviceId}`);
      }
      expectArrayIncludes(pendingUpgrade.scopes, ["operator.admin"]);
      const repaired = await getPairedDevice(identity.deviceId);
      expect(repaired?.role).toBe("operator");
      expectArrayIncludes(repaired?.approvedScopes, ["operator.read"]);
    } finally {
      ws2?.close();
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("rejects revoked device token", async () => {
    const { revokeDeviceToken } = await import("../infra/device-pairing.js");
    const { server, ws, port, prevToken } = await startControlUiServerWithClient("secret");
    const { identity, deviceToken, deviceIdentityPath } =
      await ensurePairedDeviceTokenForCurrentIdentity(ws);

    await revokeDeviceToken({ deviceId: identity.deviceId, role: "operator" });

    ws.close();

    const ws2 = await openWs(port);
    const res2 = await connectReq(ws2, { token: deviceToken, deviceIdentityPath });
    expect(res2.ok).toBe(false);

    ws2.close();
    await server.close();
    if (prevToken === undefined) {
      delete process.env.OPENCLAW_GATEWAY_TOKEN;
    } else {
      process.env.OPENCLAW_GATEWAY_TOKEN = prevToken;
    }
  });

  test("allows gateway backend loopback shared-auth connections without device pairing", async () => {
    const { server, ws, port, prevToken } = await startControlUiServerWithClient("secret");
    const sockets = [ws];
    try {
      const backendCases: Array<{
        name: string;
        headers?: Record<string, string>;
        socket?: WebSocket;
      }> = [
        { name: "default host", socket: ws },
        { name: "remote-looking host", headers: { host: "gateway.example" } },
        { name: "private host", headers: { host: "172.17.0.2:18789" } },
      ];

      for (const backendCase of backendCases) {
        const socket = backendCase.socket ?? (await openWs(port, backendCase.headers));
        if (!backendCase.socket) {
          sockets.push(socket);
        }
        const backendConnect = await connectReq(socket, {
          token: "secret",
          client: BACKEND_GATEWAY_CLIENT,
        });
        expect(backendConnect.ok, backendCase.name).toBe(true);
      }
    } finally {
      for (const socket of sockets) {
        socket.close();
      }
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("auto-approves Docker-style CLI connects on loopback with a private host header", async () => {
    const { getPairedDevice, listDevicePairing } = await import("../infra/device-pairing.js");
    const { server, port, prevToken } = await startControlUiServer("secret");
    const wsDockerCli = await openWs(port, { host: "172.17.0.2:18789" });
    try {
      const { identity, identityPath } =
        await createOperatorIdentityFixture("openclaw-cli-docker-");
      const nonce = await readConnectChallengeNonce(wsDockerCli);
      const dockerCli = await connectReq(wsDockerCli, {
        token: "secret",
        client: {
          id: GATEWAY_CLIENT_NAMES.CLI,
          version: "1.0.0",
          platform: "linux",
          mode: GATEWAY_CLIENT_MODES.CLI,
        },
        device: await buildSignedDeviceForIdentity({
          identityPath,
          client: {
            id: GATEWAY_CLIENT_NAMES.CLI,
            mode: GATEWAY_CLIENT_MODES.CLI,
          },
          scopes: ["operator.admin"],
          nonce,
        }),
      });
      expect(dockerCli.ok).toBe(true);
      const pending = await listDevicePairing();
      expect(pending.pending.filter((entry) => entry.deviceId === identity.deviceId)).toEqual([]);
      if (!(await getPairedDevice(identity.deviceId))) {
        throw new Error(`expected paired device ${identity.deviceId}`);
      }
    } finally {
      wsDockerCli.close();
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });

  test("allows CLI clients on loopback even when the host header is not private-or-loopback", async () => {
    const { server, port, prevToken } = await startControlUiServer("secret");
    const wsRemoteLike = await openWs(port, { host: "gateway.example" });
    try {
      const remoteCli = await connectReq(wsRemoteLike, {
        token: "secret",
        client: {
          id: GATEWAY_CLIENT_NAMES.CLI,
          version: "1.0.0",
          platform: "linux",
          mode: GATEWAY_CLIENT_MODES.CLI,
        },
      });
      expect(remoteCli.ok).toBe(true);
    } finally {
      wsRemoteLike.close();
      await server.close();
      restoreGatewayToken(prevToken);
    }
  });
}
