mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 10:21:37 +00:00
normalizeRoleList in src/shared/device-pairing-access.ts called .trim() on every roles[] entry and the singular role without a typeof === "string" guard, so a malformed/legacy on-disk pairing record (roles/role loaded via blind-cast JSON in coercePairingStateRecord) threw "TypeError: role.trim is not a function" and crashed resolvePendingDeviceApprovalState -- and thus `openclaw devices list`, which calls it per pending request with no try/catch. Route each item through the shared non-string-safe normalizer normalizeUniqueSingleOrTrimmedStringList, mirroring the #90654/#92178 fix that already guarded the sibling mergeRoles/mergeScopes (src/infra/device-pairing.ts) and the in-file scopes path (normalizeDeviceAuthScopes). Non-string entries are dropped; valid roles are still trimmed, deduped, and sorted. Net -10 LOC. Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
3.1 KiB
TypeScript
114 lines
3.1 KiB
TypeScript
// Device pairing access tests cover role and scope checks for pairing requests.
|
|
import { describe, expect, it } from "vitest";
|
|
import { resolvePendingDeviceApprovalState } from "./device-pairing-access.js";
|
|
|
|
describe("resolvePendingDeviceApprovalState", () => {
|
|
it("treats legacy singular approved role fields as approved access", () => {
|
|
expect(
|
|
resolvePendingDeviceApprovalState(
|
|
{
|
|
role: "operator",
|
|
scopes: ["operator.read"],
|
|
},
|
|
{
|
|
role: "operator",
|
|
scopes: ["operator.read"],
|
|
},
|
|
),
|
|
).toEqual({
|
|
kind: "re-approval",
|
|
requested: {
|
|
roles: ["operator"],
|
|
scopes: ["operator.read"],
|
|
},
|
|
approved: {
|
|
roles: ["operator"],
|
|
scopes: ["operator.read"],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("treats revoked approved-role tokens as a role upgrade", () => {
|
|
expect(
|
|
resolvePendingDeviceApprovalState(
|
|
{
|
|
role: "operator",
|
|
scopes: ["operator.read"],
|
|
},
|
|
{
|
|
role: "operator",
|
|
scopes: ["operator.read"],
|
|
tokens: {
|
|
operator: {
|
|
role: "operator",
|
|
revokedAtMs: Date.now(),
|
|
},
|
|
},
|
|
},
|
|
),
|
|
).toEqual({
|
|
kind: "role-upgrade",
|
|
requested: {
|
|
roles: ["operator"],
|
|
scopes: ["operator.read"],
|
|
},
|
|
approved: {
|
|
roles: [],
|
|
scopes: ["operator.read"],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("drops non-string role entries from malformed pairing records instead of crashing", () => {
|
|
// Legacy/malformed on-disk pairing records can carry non-string roles/role (blind-cast JSON);
|
|
// before the shared-normalizer guard these crashed normalizeRoleList on .trim().
|
|
type PendingArg = Parameters<typeof resolvePendingDeviceApprovalState>[0];
|
|
type PairedArg = NonNullable<Parameters<typeof resolvePendingDeviceApprovalState>[1]>;
|
|
expect(
|
|
resolvePendingDeviceApprovalState(
|
|
{
|
|
roles: [123, "operator", null, " admin "],
|
|
role: 5,
|
|
scopes: ["operator.read"],
|
|
} as unknown as PendingArg,
|
|
{
|
|
roles: [null, "operator"],
|
|
role: 9,
|
|
scopes: ["operator.read"],
|
|
} as unknown as PairedArg,
|
|
),
|
|
).toEqual({
|
|
kind: "role-upgrade",
|
|
requested: {
|
|
roles: ["admin", "operator"],
|
|
scopes: ["operator.read"],
|
|
},
|
|
approved: {
|
|
roles: ["operator"],
|
|
scopes: ["operator.read"],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("drops a non-string token role without crashing", () => {
|
|
type PairedArg = NonNullable<Parameters<typeof resolvePendingDeviceApprovalState>[1]>;
|
|
expect(
|
|
resolvePendingDeviceApprovalState({ role: "operator", scopes: ["operator.read"] }, {
|
|
roles: ["operator"],
|
|
scopes: ["operator.read"],
|
|
tokens: { t1: { role: 7, revokedAtMs: null } },
|
|
} as unknown as PairedArg),
|
|
).toEqual({
|
|
kind: "role-upgrade",
|
|
requested: {
|
|
roles: ["operator"],
|
|
scopes: ["operator.read"],
|
|
},
|
|
approved: {
|
|
roles: [],
|
|
scopes: ["operator.read"],
|
|
},
|
|
});
|
|
});
|
|
});
|