Skip to content

Commit 5002769

Browse files
committed
fix(device-pairing): align subset pairing with scope coverage
1 parent b0caab3 commit 5002769

2 files changed

Lines changed: 46 additions & 4 deletions

File tree

src/infra/device-pairing-churn.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,34 @@ describe("device pairing requestId churn", () => {
167167
expect(approved?.status).toBe("approved");
168168
});
169169

170+
test("keeps the requestId when operator.admin covers a pairing reconnect", async () => {
171+
const baseDir = await makeDevicePairingDir();
172+
173+
const adminRequest = await requestDevicePairing(
174+
{
175+
deviceId: DEVICE_ID,
176+
publicKey: PUBLIC_KEY,
177+
role: "operator",
178+
scopes: ["operator.admin"],
179+
},
180+
baseDir,
181+
);
182+
183+
const pairingReconnect = await requestDevicePairing(
184+
{
185+
deviceId: DEVICE_ID,
186+
publicKey: PUBLIC_KEY,
187+
role: "operator",
188+
scopes: ["operator.pairing"],
189+
},
190+
baseDir,
191+
);
192+
193+
expect(pairingReconnect.created).toBe(false);
194+
expect(pairingReconnect.request.requestId).toBe(adminRequest.request.requestId);
195+
expect(pairingReconnect.request.scopes).toEqual(["operator.admin"]);
196+
});
197+
170198
test("supports cron-first progressive operator escalation from read to pairing to admin", async () => {
171199
const baseDir = await makeDevicePairingDir();
172200

src/infra/device-pairing.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,10 +374,24 @@ function incomingApprovalCoveredByExisting(
374374
if (normalizeRole(existing.role) !== normalizeRole(incoming.role)) {
375375
return false;
376376
}
377-
return (
378-
isStringSubset(resolveRequestedRoles(incoming), resolveRequestedRoles(existing)) &&
379-
isStringSubset(resolveRequestedScopes(incoming), resolveRequestedScopes(existing))
380-
);
377+
const incomingRoles = resolveRequestedRoles(incoming);
378+
if (!isStringSubset(incomingRoles, resolveRequestedRoles(existing))) {
379+
return false;
380+
}
381+
const existingScopes = resolveRequestedScopes(existing);
382+
for (const scope of resolveRequestedScopes(incoming)) {
383+
const covered = incomingRoles.some((role) =>
384+
roleScopesAllow({
385+
role,
386+
requestedScopes: [scope],
387+
allowedScopes: existingScopes,
388+
}),
389+
);
390+
if (!covered) {
391+
return false;
392+
}
393+
}
394+
return true;
381395
}
382396

383397
function refreshPendingDevicePairingRequest(

0 commit comments

Comments
 (0)