fix(gateway): revoke active sessions on token rotation (#57646)

This commit is contained in:
Vincent Koc
2026-03-31 09:05:34 +09:00
committed by GitHub
parent bd957a3a8b
commit 91f7a6b0fd
3 changed files with 77 additions and 1 deletions

View File

@@ -2,9 +2,16 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { deviceHandlers } from "./devices.js";
import type { GatewayRequestHandlerOptions } from "./types.js";
const { removePairedDeviceMock, revokeDeviceTokenMock } = vi.hoisted(() => ({
const {
getPairedDeviceMock,
removePairedDeviceMock,
revokeDeviceTokenMock,
rotateDeviceTokenMock,
} = vi.hoisted(() => ({
getPairedDeviceMock: vi.fn(),
removePairedDeviceMock: vi.fn(),
revokeDeviceTokenMock: vi.fn(),
rotateDeviceTokenMock: vi.fn(),
}));
vi.mock("../../infra/device-pairing.js", async () => {
@@ -13,8 +20,10 @@ vi.mock("../../infra/device-pairing.js", async () => {
);
return {
...actual,
getPairedDevice: getPairedDeviceMock,
removePairedDevice: removePairedDeviceMock,
revokeDeviceToken: revokeDeviceTokenMock,
rotateDeviceToken: rotateDeviceTokenMock,
};
});
@@ -101,6 +110,69 @@ describe("deviceHandlers", () => {
);
});
it("disconnects active clients after rotating a device token", async () => {
getPairedDeviceMock.mockResolvedValue({
deviceId: "device-1",
scopes: ["operator.pairing"],
tokens: {
operator: {
token: "old-token",
role: "operator",
scopes: ["operator.pairing"],
createdAtMs: 123,
},
},
});
rotateDeviceTokenMock.mockResolvedValue({
ok: true,
entry: {
token: "new-token",
role: "operator",
scopes: ["operator.pairing"],
createdAtMs: 456,
rotatedAtMs: 789,
},
});
const opts = createOptions(
"device.token.rotate",
{
deviceId: " device-1 ",
role: " operator ",
scopes: ["operator.pairing"],
},
{
client: {
connect: {
scopes: ["operator.pairing"],
},
} as never,
},
);
await deviceHandlers["device.token.rotate"](opts);
await Promise.resolve();
expect(rotateDeviceTokenMock).toHaveBeenCalledWith({
deviceId: " device-1 ",
role: " operator ",
scopes: ["operator.pairing"],
});
expect(opts.context.disconnectClientsForDevice).toHaveBeenCalledWith("device-1", {
role: "operator",
});
expect(opts.respond).toHaveBeenCalledWith(
true,
{
deviceId: " device-1 ",
role: "operator",
token: "new-token",
scopes: ["operator.pairing"],
rotatedAtMs: 789,
},
undefined,
);
});
it("does not disconnect clients when token revocation fails", async () => {
revokeDeviceTokenMock.mockResolvedValue(null);
const opts = createOptions("device.token.revoke", {

View File

@@ -265,6 +265,9 @@ export const deviceHandlers: GatewayRequestHandlers = {
},
undefined,
);
queueMicrotask(() => {
context.disconnectClientsForDevice?.(deviceId.trim(), { role: entry.role });
});
},
"device.token.revoke": async ({ params, respond, context }) => {
if (!validateDeviceTokenRevokeParams(params)) {