mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 07:01:24 +00:00
fix(gateway): clear pairing state on device token mismatch (#22071)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: ad38d1a529
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:
@@ -4,6 +4,7 @@ import type { DeviceIdentity } from "../infra/device-identity.js";
|
||||
|
||||
const wsInstances = vi.hoisted((): MockWebSocket[] => []);
|
||||
const clearDeviceAuthTokenMock = vi.hoisted(() => vi.fn());
|
||||
const clearDevicePairingMock = vi.hoisted(() => vi.fn());
|
||||
const logDebugMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
type WsEvent = "open" | "message" | "close" | "error";
|
||||
@@ -68,6 +69,14 @@ vi.mock("../infra/device-auth-store.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../infra/device-pairing.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../infra/device-pairing.js")>();
|
||||
return {
|
||||
...actual,
|
||||
clearDevicePairing: (...args: unknown[]) => clearDevicePairingMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../logger.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../logger.js")>();
|
||||
return {
|
||||
@@ -161,6 +170,8 @@ describe("GatewayClient close handling", () => {
|
||||
beforeEach(() => {
|
||||
wsInstances.length = 0;
|
||||
clearDeviceAuthTokenMock.mockReset();
|
||||
clearDevicePairingMock.mockReset();
|
||||
clearDevicePairingMock.mockResolvedValue(true);
|
||||
logDebugMock.mockReset();
|
||||
});
|
||||
|
||||
@@ -184,6 +195,7 @@ describe("GatewayClient close handling", () => {
|
||||
);
|
||||
|
||||
expect(clearDeviceAuthTokenMock).toHaveBeenCalledWith({ deviceId: "dev-1", role: "operator" });
|
||||
expect(clearDevicePairingMock).toHaveBeenCalledWith("dev-1");
|
||||
expect(onClose).toHaveBeenCalledWith(
|
||||
1008,
|
||||
"unauthorized: DEVICE token mismatch (rotate/reissue device token)",
|
||||
@@ -215,6 +227,34 @@ describe("GatewayClient close handling", () => {
|
||||
expect(logDebugMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("failed clearing stale device-auth token"),
|
||||
);
|
||||
expect(clearDevicePairingMock).not.toHaveBeenCalled();
|
||||
expect(onClose).toHaveBeenCalledWith(1008, "unauthorized: device token mismatch");
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it("does not break close flow when pairing clear rejects", async () => {
|
||||
clearDevicePairingMock.mockRejectedValue(new Error("pairing store unavailable"));
|
||||
const onClose = vi.fn();
|
||||
const identity: DeviceIdentity = {
|
||||
deviceId: "dev-3",
|
||||
privateKeyPem: "private-key",
|
||||
publicKeyPem: "public-key",
|
||||
};
|
||||
const client = new GatewayClient({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
deviceIdentity: identity,
|
||||
onClose,
|
||||
});
|
||||
|
||||
client.start();
|
||||
expect(() => {
|
||||
getLatestWs().emitClose(1008, "unauthorized: device token mismatch");
|
||||
}).not.toThrow();
|
||||
|
||||
await Promise.resolve();
|
||||
expect(logDebugMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("failed clearing stale device pairing"),
|
||||
);
|
||||
expect(onClose).toHaveBeenCalledWith(1008, "unauthorized: device token mismatch");
|
||||
client.stop();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
publicKeyRawBase64UrlFromPem,
|
||||
signDevicePayload,
|
||||
} from "../infra/device-identity.js";
|
||||
import { clearDevicePairing } from "../infra/device-pairing.js";
|
||||
import { normalizeFingerprint } from "../infra/tls/fingerprint.js";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
import { logDebug, logError } from "../logger.js";
|
||||
@@ -175,21 +176,23 @@ export class GatewayClient {
|
||||
this.ws.on("close", (code, reason) => {
|
||||
const reasonText = rawDataToString(reason);
|
||||
this.ws = null;
|
||||
// If closed due to device token mismatch, clear the stored token so next attempt can get a fresh one
|
||||
// If closed due to device token mismatch, clear the stored token and pairing so next attempt can get a fresh one
|
||||
if (
|
||||
code === 1008 &&
|
||||
reasonText.toLowerCase().includes("device token mismatch") &&
|
||||
this.opts.deviceIdentity
|
||||
) {
|
||||
const deviceId = this.opts.deviceIdentity.deviceId;
|
||||
const role = this.opts.role ?? "operator";
|
||||
try {
|
||||
clearDeviceAuthToken({ deviceId: this.opts.deviceIdentity.deviceId, role });
|
||||
logDebug(
|
||||
`cleared stale device-auth token for device ${this.opts.deviceIdentity.deviceId}`,
|
||||
);
|
||||
clearDeviceAuthToken({ deviceId, role });
|
||||
void clearDevicePairing(deviceId).catch((err) => {
|
||||
logDebug(`failed clearing stale device pairing for device ${deviceId}: ${String(err)}`);
|
||||
});
|
||||
logDebug(`cleared stale device-auth token for device ${deviceId}`);
|
||||
} catch (err) {
|
||||
logDebug(
|
||||
`failed clearing stale device-auth token for device ${this.opts.deviceIdentity.deviceId}: ${String(err)}`,
|
||||
`failed clearing stale device-auth token for device ${deviceId}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user