fix(pairing): clear stale requests on device removal (#70239) · openclaw/openclaw@dd46783
drobison00
·
2026-04-23
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -56,6 +56,7 @@ Docs: https://docs.openclaw.ai
|
56 | 56 | - 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. |
57 | 57 | - 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. |
58 | 58 | - Gateway/restart: write restart sentinel files atomically so interrupted writes cannot leave a truncated sentinel behind. (#70225) Thanks @obviyus. |
| 59 | +- 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. |
59 | 60 | |
60 | 61 | ## 2026.4.21 |
61 | 62 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1013,6 +1013,46 @@ describe("device pairing tokens", () => {
|
1013 | 1013 | await expect(removePairedDevice("device-1", baseDir)).resolves.toBeNull(); |
1014 | 1014 | }); |
1015 | 1015 | |
| 1016 | +test("removing a paired device clears pending requests for that device only", async () => { |
| 1017 | +const baseDir = await makeDevicePairingDir(); |
| 1018 | +await setupPairedOperatorDevice(baseDir, ["operator.read"]); |
| 1019 | + |
| 1020 | +const staleRepair = await requestDevicePairing( |
| 1021 | +{ |
| 1022 | +deviceId: "device-1", |
| 1023 | +publicKey: "public-key-1-rotated", |
| 1024 | +role: "operator", |
| 1025 | +scopes: ["operator.read"], |
| 1026 | +}, |
| 1027 | +baseDir, |
| 1028 | +); |
| 1029 | +const otherPending = await requestDevicePairing( |
| 1030 | +{ |
| 1031 | +deviceId: "device-2", |
| 1032 | +publicKey: "public-key-2", |
| 1033 | +role: "node", |
| 1034 | +scopes: [], |
| 1035 | +}, |
| 1036 | +baseDir, |
| 1037 | +); |
| 1038 | + |
| 1039 | +await expect(removePairedDevice("device-1", baseDir)).resolves.toEqual({ |
| 1040 | +deviceId: "device-1", |
| 1041 | +}); |
| 1042 | + |
| 1043 | +const pending = (await listDevicePairing(baseDir)).pending; |
| 1044 | +expect(pending.map((entry) => entry.requestId)).not.toContain(staleRepair.request.requestId); |
| 1045 | +expect(pending.map((entry) => entry.requestId)).toContain(otherPending.request.requestId); |
| 1046 | +await expect( |
| 1047 | +approveDevicePairing( |
| 1048 | +staleRepair.request.requestId, |
| 1049 | +{ callerScopes: ["operator.read"] }, |
| 1050 | +baseDir, |
| 1051 | +), |
| 1052 | +).resolves.toBeNull(); |
| 1053 | +await expect(getPairedDevice("device-1", baseDir)).resolves.toBeNull(); |
| 1054 | +}); |
| 1055 | + |
1016 | 1056 | test("clears paired device state by device id", async () => { |
1017 | 1057 | const baseDir = await makeDevicePairingDir(); |
1018 | 1058 | await setupPairedOperatorDevice(baseDir, ["operator.read"]); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -758,6 +758,11 @@ export async function removePairedDevice(
|
758 | 758 | return null; |
759 | 759 | } |
760 | 760 | delete state.pairedByDeviceId[normalized]; |
| 761 | +for (const [requestId, pending] of Object.entries(state.pendingById)) { |
| 762 | +if (pending.deviceId === normalized) { |
| 763 | +delete state.pendingById[requestId]; |
| 764 | +} |
| 765 | +} |
761 | 766 | await persistState(state, baseDir); |
762 | 767 | return { deviceId: normalized }; |
763 | 768 | }); |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。