mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
fix(pairing): clear stale requests on device removal (#70239)
* fix(pairing): clear stale requests on device removal * docs(changelog): note pairing stale request cleanup
This commit is contained in:
@@ -56,6 +56,7 @@ Docs: https://docs.openclaw.ai
|
||||
- QQBot: add `INTERACTION` intent (`1 << 26`) to the gateway constants and include it in the `FULL_INTENTS` mask so interaction events are received. (#70143) Thanks @cxyhhhhh.
|
||||
- Gateway/restart: preserve one-shot continuation instructions across gateway restarts so agents can resume and reply back to the original chat after reboot. (#63406) Thanks @VACInc.
|
||||
- Gateway/restart: write restart sentinel files atomically so interrupted writes cannot leave a truncated sentinel behind. (#70225) Thanks @obviyus.
|
||||
- Pairing: remove stale pending requests for a device when that paired device is deleted, so an old repair approval cannot recreate the removed device from leftover state.
|
||||
|
||||
## 2026.4.21
|
||||
|
||||
|
||||
@@ -1013,6 +1013,46 @@ describe("device pairing tokens", () => {
|
||||
await expect(removePairedDevice("device-1", baseDir)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
test("removing a paired device clears pending requests for that device only", async () => {
|
||||
const baseDir = await makeDevicePairingDir();
|
||||
await setupPairedOperatorDevice(baseDir, ["operator.read"]);
|
||||
|
||||
const staleRepair = await requestDevicePairing(
|
||||
{
|
||||
deviceId: "device-1",
|
||||
publicKey: "public-key-1-rotated",
|
||||
role: "operator",
|
||||
scopes: ["operator.read"],
|
||||
},
|
||||
baseDir,
|
||||
);
|
||||
const otherPending = await requestDevicePairing(
|
||||
{
|
||||
deviceId: "device-2",
|
||||
publicKey: "public-key-2",
|
||||
role: "node",
|
||||
scopes: [],
|
||||
},
|
||||
baseDir,
|
||||
);
|
||||
|
||||
await expect(removePairedDevice("device-1", baseDir)).resolves.toEqual({
|
||||
deviceId: "device-1",
|
||||
});
|
||||
|
||||
const pending = (await listDevicePairing(baseDir)).pending;
|
||||
expect(pending.map((entry) => entry.requestId)).not.toContain(staleRepair.request.requestId);
|
||||
expect(pending.map((entry) => entry.requestId)).toContain(otherPending.request.requestId);
|
||||
await expect(
|
||||
approveDevicePairing(
|
||||
staleRepair.request.requestId,
|
||||
{ callerScopes: ["operator.read"] },
|
||||
baseDir,
|
||||
),
|
||||
).resolves.toBeNull();
|
||||
await expect(getPairedDevice("device-1", baseDir)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
test("clears paired device state by device id", async () => {
|
||||
const baseDir = await makeDevicePairingDir();
|
||||
await setupPairedOperatorDevice(baseDir, ["operator.read"]);
|
||||
|
||||
@@ -758,6 +758,11 @@ export async function removePairedDevice(
|
||||
return null;
|
||||
}
|
||||
delete state.pairedByDeviceId[normalized];
|
||||
for (const [requestId, pending] of Object.entries(state.pendingById)) {
|
||||
if (pending.deviceId === normalized) {
|
||||
delete state.pendingById[requestId];
|
||||
}
|
||||
}
|
||||
await persistState(state, baseDir);
|
||||
return { deviceId: normalized };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user