Hardening: refresh stale device pairing requests and pending metadata (#50695)

* Docs: clarify device pairing supersede behavior

* Device pairing: supersede pending requests on auth changes
This commit is contained in:
Josh Avant
2026-03-19 18:26:06 -05:00
committed by GitHub
parent 9486f6e379
commit 8e132aed6e
19 changed files with 452 additions and 41 deletions

View File

@@ -287,6 +287,30 @@ describe("devices cli local fallback", () => {
});
});
describe("devices cli list", () => {
it("renders pending scopes when present", async () => {
callGateway.mockResolvedValueOnce({
pending: [
{
requestId: "req-1",
deviceId: "device-1",
displayName: "Device One",
role: "operator",
scopes: ["operator.admin", "operator.read"],
ts: 1,
},
],
paired: [],
});
await runDevicesCommand(["list"]);
const output = runtime.log.mock.calls.map((entry) => String(entry[0] ?? "")).join("\n");
expect(output).toContain("Scopes");
expect(output).toContain("operator.admin, operator.read");
});
});
afterEach(() => {
callGateway.mockClear();
buildGatewayConnectionDetails.mockClear();

View File

@@ -39,6 +39,8 @@ type PendingDevice = {
deviceId: string;
displayName?: string;
role?: string;
roles?: string[];
scopes?: string[];
remoteIp?: string;
isRepair?: boolean;
ts?: number;
@@ -197,6 +199,30 @@ function formatTokenSummary(tokens: DeviceTokenSummary[] | undefined) {
return parts.join(", ");
}
function formatPendingRoles(request: PendingDevice): string {
const role = typeof request.role === "string" ? request.role.trim() : "";
if (role) {
return role;
}
const roles = Array.isArray(request.roles)
? request.roles.map((item) => item.trim()).filter((item) => item.length > 0)
: [];
if (roles.length === 0) {
return "";
}
return roles.join(", ");
}
function formatPendingScopes(request: PendingDevice): string {
const scopes = Array.isArray(request.scopes)
? request.scopes.map((item) => item.trim()).filter((item) => item.length > 0)
: [];
if (scopes.length === 0) {
return "";
}
return scopes.join(", ");
}
function resolveRequiredDeviceRole(
opts: DevicesRpcOpts,
): { deviceId: string; role: string } | null {
@@ -235,6 +261,7 @@ export function registerDevicesCli(program: Command) {
{ key: "Request", header: "Request", minWidth: 10 },
{ key: "Device", header: "Device", minWidth: 16, flex: true },
{ key: "Role", header: "Role", minWidth: 8 },
{ key: "Scopes", header: "Scopes", minWidth: 14, flex: true },
{ key: "IP", header: "IP", minWidth: 12 },
{ key: "Age", header: "Age", minWidth: 8 },
{ key: "Flags", header: "Flags", minWidth: 8 },
@@ -242,7 +269,8 @@ export function registerDevicesCli(program: Command) {
rows: list.pending.map((req) => ({
Request: req.requestId,
Device: req.displayName || req.deviceId,
Role: req.role ?? "",
Role: formatPendingRoles(req),
Scopes: formatPendingScopes(req),
IP: req.remoteIp ?? "",
Age: typeof req.ts === "number" ? formatTimeAgo(Date.now() - req.ts) : "",
Flags: req.isRepair ? "repair" : "",