fix(pairing): mint tokens for merged device roles

This commit is contained in:
Ayaan Zaidi
2026-04-03 15:16:30 +05:30
parent 61f13173c2
commit 403e0e6521
3 changed files with 58 additions and 12 deletions

View File

@@ -206,6 +206,33 @@ describe("device pairing tokens", () => {
status: "approved",
requestId: request.request.requestId,
});
const paired = await getPairedDevice("device-1", baseDir);
expect(paired && listEffectivePairedDeviceRoles(paired)).toEqual(["node", "operator"]);
expect(paired?.tokens?.node?.scopes).toEqual([]);
expect(paired?.tokens?.operator?.scopes).toEqual([
"operator.read",
"operator.talk.secrets",
"operator.write",
]);
await expect(
verifyDeviceToken({
deviceId: "device-1",
token: requireToken(paired?.tokens?.node?.token),
role: "node",
scopes: [],
baseDir,
}),
).resolves.toEqual({ ok: true });
await expect(
verifyDeviceToken({
deviceId: "device-1",
token: requireToken(paired?.tokens?.operator?.token),
role: "operator",
scopes: ["operator.read"],
baseDir,
}),
).resolves.toEqual({ ok: true });
});
test("keeps superseded requests interactive when an existing pending request is interactive", async () => {

View File

@@ -344,6 +344,28 @@ function buildDeviceAuthToken(params: {
};
}
function resolveApprovedTokenScopes(params: {
role: string;
pending: DevicePairingPendingRequest;
existingToken?: DeviceAuthToken;
approvedScopes?: string[];
existing?: PairedDevice;
}): string[] {
if (params.role === "operator") {
const requestedScopes = normalizeDeviceAuthScopes(params.pending.scopes);
if (requestedScopes.length > 0) {
return requestedScopes;
}
return normalizeDeviceAuthScopes(
params.existingToken?.scopes ??
params.approvedScopes ??
params.existing?.approvedScopes ??
params.existing?.scopes,
);
}
return normalizeDeviceAuthScopes(params.existingToken?.scopes);
}
function resolveApprovedDeviceScopeBaseline(device: PairedDevice): string[] | null {
const baseline = device.approvedScopes ?? device.scopes;
if (!Array.isArray(baseline)) {
@@ -506,19 +528,15 @@ export async function approveDevicePairing(
pending.scopes,
);
const tokens = existing?.tokens ? { ...existing.tokens } : {};
const roleForToken = normalizeRole(pending.role);
if (roleForToken) {
for (const roleForToken of requestedRoles) {
const existingToken = tokens[roleForToken];
const requestedScopes = normalizeDeviceAuthScopes(pending.scopes);
const nextScopes =
requestedScopes.length > 0
? requestedScopes
: normalizeDeviceAuthScopes(
existingToken?.scopes ??
approvedScopes ??
existing?.approvedScopes ??
existing?.scopes,
);
const nextScopes = resolveApprovedTokenScopes({
role: roleForToken,
pending,
existingToken,
approvedScopes,
existing,
});
const now = Date.now();
tokens[roleForToken] = {
token: newToken(),