Gateway/CLI: add paired-device remove and clear flows (#20057)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 26523f8a38
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-02-18 13:27:31 +00:00
committed by GitHub
parent fc65f70a9b
commit 1437ed76a0
13 changed files with 239 additions and 2 deletions

View File

@@ -5,6 +5,7 @@ import { describe, expect, test } from "vitest";
import {
approveDevicePairing,
getPairedDevice,
removePairedDevice,
requestDevicePairing,
rotateDeviceToken,
verifyDeviceToken,
@@ -109,4 +110,15 @@ describe("device pairing tokens", () => {
}),
).resolves.toEqual({ ok: false, reason: "token-mismatch" });
});
test("removes paired devices by device id", async () => {
const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-"));
await setupPairedOperatorDevice(baseDir, ["operator.read"]);
const removed = await removePairedDevice("device-1", baseDir);
expect(removed).toEqual({ deviceId: "device-1" });
await expect(getPairedDevice("device-1", baseDir)).resolves.toBeNull();
await expect(removePairedDevice("device-1", baseDir)).resolves.toBeNull();
});
});

View File

@@ -321,6 +321,22 @@ export async function rejectDevicePairing(
});
}
export async function removePairedDevice(
deviceId: string,
baseDir?: string,
): Promise<{ deviceId: string } | null> {
return await withLock(async () => {
const state = await loadState(baseDir);
const normalized = normalizeDeviceId(deviceId);
if (!normalized || !state.pairedByDeviceId[normalized]) {
return null;
}
delete state.pairedByDeviceId[normalized];
await persistState(state, baseDir);
return { deviceId: normalized };
});
}
export async function updatePairedDeviceMetadata(
deviceId: string,
patch: Partial<Omit<PairedDevice, "deviceId" | "createdAtMs" | "approvedAtMs">>,