Gateway: preserve token scopes on scope-less repair approvals

This commit is contained in:
Vignesh Natarajan
2026-02-21 19:37:15 -08:00
parent 55d492b4cd
commit 483c464b62
3 changed files with 31 additions and 1 deletions

View File

@@ -122,6 +122,26 @@ describe("device pairing tokens", () => {
expect(paired?.tokens?.operator?.scopes).toEqual(["operator.read"]);
});
test("preserves existing token scopes when approving a repair without requested scopes", async () => {
const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-"));
await setupPairedOperatorDevice(baseDir, ["operator.admin"]);
const repair = await requestDevicePairing(
{
deviceId: "device-1",
publicKey: "public-key-1",
role: "operator",
},
baseDir,
);
await approveDevicePairing(repair.request.requestId, baseDir);
const paired = await getPairedDevice("device-1", baseDir);
expect(paired?.scopes).toEqual(["operator.admin"]);
expect(paired?.approvedScopes).toEqual(["operator.admin"]);
expect(paired?.tokens?.operator?.scopes).toEqual(["operator.admin"]);
});
test("rejects scope escalation when rotating a token and leaves state unchanged", async () => {
const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-"));
await setupPairedOperatorDevice(baseDir, ["operator.read"]);

View File

@@ -332,8 +332,17 @@ export async function approveDevicePairing(
const tokens = existing?.tokens ? { ...existing.tokens } : {};
const roleForToken = normalizeRole(pending.role);
if (roleForToken) {
const nextScopes = normalizeDeviceAuthScopes(pending.scopes);
const existingToken = tokens[roleForToken];
const requestedScopes = normalizeDeviceAuthScopes(pending.scopes);
const nextScopes =
requestedScopes.length > 0
? requestedScopes
: normalizeDeviceAuthScopes(
existingToken?.scopes ??
approvedScopes ??
existing?.approvedScopes ??
existing?.scopes,
);
const now = Date.now();
tokens[roleForToken] = {
token: newToken(),