mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:40:44 +00:00
fix(doctor): harden pairing health notes
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
import {
|
||||
approveDevicePairing,
|
||||
requestDevicePairing,
|
||||
revokeDeviceToken,
|
||||
rotateDeviceToken,
|
||||
} from "../infra/device-pairing.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
@@ -183,4 +184,81 @@ describe("noteDevicePairingHealth", () => {
|
||||
expect(noteMock).toHaveBeenCalledTimes(1);
|
||||
expect(String(noteMock.mock.calls[0]?.[0] ?? "")).toContain("req-gateway-1");
|
||||
});
|
||||
|
||||
it("sanitizes device labels before printing doctor notes", async () => {
|
||||
callGatewayMock.mockResolvedValue({
|
||||
pending: [
|
||||
{
|
||||
requestId: "req-gateway-1",
|
||||
deviceId: "device-gateway-1",
|
||||
publicKey: "pubkey",
|
||||
role: "operator",
|
||||
roles: ["operator"],
|
||||
scopes: ["operator.admin"],
|
||||
clientId: "control-ui\tclient",
|
||||
clientMode: "webchat",
|
||||
displayName: "\u001b[2Kbad\nname",
|
||||
ts: 1,
|
||||
isRepair: false,
|
||||
},
|
||||
],
|
||||
paired: [],
|
||||
});
|
||||
|
||||
await noteDevicePairingHealth({
|
||||
cfg: { gateway: { mode: "remote" } },
|
||||
healthOk: true,
|
||||
});
|
||||
|
||||
const message = String(noteMock.mock.calls[0]?.[0] ?? "");
|
||||
expect(message).toContain("bad\\nname");
|
||||
expect(message).not.toContain("\u001b");
|
||||
expect(message).not.toContain("control-ui\tclient");
|
||||
});
|
||||
|
||||
it("does not duplicate missing-token warnings when local cache exists for an approved role", async () => {
|
||||
await withTempDir("openclaw-doctor-device-pairing-", async (stateDir) => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
OPENCLAW_TEST_FAST: "1",
|
||||
},
|
||||
async () => {
|
||||
const identity = loadOrCreateDeviceIdentity();
|
||||
const publicKey = publicKeyRawBase64UrlFromPem(identity.publicKeyPem);
|
||||
const initial = await requestDevicePairing({
|
||||
deviceId: identity.deviceId,
|
||||
publicKey,
|
||||
role: "operator",
|
||||
scopes: ["operator.read"],
|
||||
clientId: "control-ui",
|
||||
clientMode: "webchat",
|
||||
displayName: "Dashboard",
|
||||
});
|
||||
await approveDevicePairing(initial.request.requestId, {
|
||||
callerScopes: ["operator.read"],
|
||||
});
|
||||
storeDeviceAuthToken({
|
||||
deviceId: identity.deviceId,
|
||||
role: "operator",
|
||||
token: "stale-local-token",
|
||||
scopes: ["operator.read"],
|
||||
});
|
||||
await revokeDeviceToken({
|
||||
deviceId: identity.deviceId,
|
||||
role: "operator",
|
||||
});
|
||||
|
||||
await noteDevicePairingHealth({
|
||||
cfg: { gateway: { mode: "local" } },
|
||||
healthOk: false,
|
||||
});
|
||||
|
||||
const message = String(noteMock.mock.calls[0]?.[0] ?? "");
|
||||
expect(message).toContain("has no active operator device token");
|
||||
expect(message).not.toContain("no longer has a matching active gateway token");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { DeviceAuthStore } from "../shared/device-auth.js";
|
||||
import { normalizeDeviceAuthScopes } from "../shared/device-auth.js";
|
||||
import { roleScopesAllow } from "../shared/operator-scope-compat.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { sanitizeTerminalText } from "../terminal/safe-text.js";
|
||||
|
||||
type GatewayListedPairedDevice = Omit<PairedDevice, "tokens" | "approvedScopes"> & {
|
||||
tokens?: DeviceAuthTokenSummary[];
|
||||
@@ -188,7 +189,9 @@ function describeDevice(params: {
|
||||
displayName?: string;
|
||||
clientId?: string;
|
||||
}): string {
|
||||
const label = params.displayName?.trim() || params.clientId?.trim();
|
||||
const label =
|
||||
sanitizeTerminalText(params.displayName?.trim() || "") ||
|
||||
sanitizeTerminalText(params.clientId?.trim() || "");
|
||||
return label ? `${label} (${params.deviceId})` : params.deviceId;
|
||||
}
|
||||
|
||||
@@ -447,6 +450,7 @@ function collectLocalDeviceAuthIssues(snapshot: DoctorPairingSnapshot): string[]
|
||||
clientId: paired.clientId,
|
||||
});
|
||||
const lines: string[] = [];
|
||||
const approvedRoles = new Set(listApprovedPairedDeviceRoles(paired));
|
||||
for (const entry of Object.values(store.tokens)) {
|
||||
const role = entry.role.trim();
|
||||
if (!role) {
|
||||
@@ -457,6 +461,9 @@ function collectLocalDeviceAuthIssues(snapshot: DoctorPairingSnapshot): string[]
|
||||
);
|
||||
const pairedToken = findTokenSummary(paired, role);
|
||||
if (!pairedToken) {
|
||||
if (approvedRoles.has(role)) {
|
||||
continue;
|
||||
}
|
||||
lines.push(
|
||||
`- Local cached ${role} device auth for ${deviceLabel} no longer has a matching active gateway token. Reconnect with shared gateway auth to refresh it, or rotate with ${rotateCommand}.`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user