import { readFile, writeFile } from "node:fs/promises" ;
import { afterAll, beforeAll, describe, expect, test } from "vitest" ;
import { PAIRING_SETUP_BOOTSTRAP_PROFILE } from "../shared/device-bootstrap-profile.js" ;
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js" ;
import { issueDeviceBootstrapToken, verifyDeviceBootstrapToken } from "./device-bootstrap.js" ;
import {
approveBootstrapDevicePairing,
approveDevicePairing,
clearDevicePairing,
ensureDeviceToken,
getPairedDevice,
hasEffectivePairedDeviceRole,
listEffectivePairedDeviceRoles,
listDevicePairing,
removePairedDevice,
requestDevicePairing,
revokeDeviceToken,
rotateDeviceToken,
updatePairedDeviceMetadata,
verifyDeviceToken,
type PairedDevice,
type RotateDeviceTokenResult,
} from "./device-pairing.js" ;
import { resolvePairingPaths } from "./pairing-files.js" ;
async function setupPairedOperatorDevice(baseDir: string, scopes: string[]) {
const request = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "operator" ,
scopes,
},
baseDir,
);
await approveDevicePairing(request.request.requestId, { callerScopes: scopes }, baseDir);
}
async function setupPairedNodeDevice(baseDir: string) {
const request = await requestDevicePairing(
{
deviceId: "node-1" ,
publicKey: "public-key-node-1" ,
role: "node" ,
scopes: [],
},
baseDir,
);
await approveDevicePairing(request.request.requestId, { callerScopes: [] }, baseDir);
}
async function setupOperatorToken(scopes: string[]) {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, scopes);
const paired = await getPairedDevice("device-1" , baseDir);
const token = requireToken(paired?.tokens?.operator?.token);
return { baseDir, token };
}
function verifyOperatorToken(params: { baseDir: string; token: string; scopes: string[] }) {
return verifyDeviceToken({
deviceId: "device-1" ,
token: params.token,
role: "operator" ,
scopes: params.scopes,
baseDir: params.baseDir,
});
}
function requireToken(token: string | undefined): string {
expect(typeof token).toBe("string" );
if (typeof token !== "string" ) {
throw new Error("expected device token to be issued" );
}
return token;
}
function requireRotatedEntry(result: RotateDeviceTokenResult) {
expect(result.ok).toBe(true );
if (!result.ok) {
throw new Error(`expected rotated token entry, got ${result.reason}`);
}
return result.entry;
}
async function overwritePairedOperatorTokenScopes(baseDir: string, scopes: string[]) {
const { pairedPath } = resolvePairingPaths(baseDir, "devices" );
const pairedByDeviceId = JSON.parse(await readFile(pairedPath, "utf8" )) as Record<
string,
PairedDevice
>;
const device = pairedByDeviceId["device-1" ];
expect(device?.tokens?.operator).toBeDefined();
if (!device?.tokens?.operator) {
throw new Error("expected paired operator token" );
}
device.tokens.operator.scopes = scopes;
await writeFile(pairedPath, JSON.stringify(pairedByDeviceId, null , 2 ));
}
async function mutatePairedDevice(
baseDir: string,
deviceId: string,
mutate: (device: PairedDevice) => void ,
) {
const { pairedPath } = resolvePairingPaths(baseDir, "devices" );
const pairedByDeviceId = JSON.parse(await readFile(pairedPath, "utf8" )) as Record<
string,
PairedDevice
>;
const device = pairedByDeviceId[deviceId];
expect(device).toBeDefined();
if (!device) {
throw new Error(`expected paired device ${deviceId}`);
}
mutate(device);
await writeFile(pairedPath, JSON.stringify(pairedByDeviceId, null , 2 ));
}
async function clearPairedOperatorApprovalBaseline(baseDir: string) {
await mutatePairedDevice(baseDir, "device-1" , (device) => {
delete device.approvedScopes;
delete device.scopes;
});
}
const suiteRootTracker = createSuiteTempRootTracker({ prefix: "openclaw-device-pairing-" });
async function makeDevicePairingDir(): Promise<string> {
return await suiteRootTracker.make("case" );
}
describe("device pairing tokens" , () => {
beforeAll(async () => {
await suiteRootTracker.setup();
});
afterAll(async () => {
await suiteRootTracker.cleanup();
});
test("reuses existing pending requests for the same device" , async () => {
const baseDir = await makeDevicePairingDir();
const first = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
},
baseDir,
);
const second = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
},
baseDir,
);
expect(first.created).toBe(true );
expect(second.created).toBe(false );
expect(second.request.requestId).toBe(first.request.requestId);
});
test("re-requesting with identical params preserves the original ts to prevent queue-jumping" , async () => {
// Regression: refreshPendingDevicePairingRequest must not bump ts to Date.now().
// An attacker who reconnects with the same key/role/scopes could otherwise
// silently move their request to the top of the implicit --latest approval queue.
const baseDir = await makeDevicePairingDir();
const first = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "operator" ,
scopes: ["operator.read" ],
},
baseDir,
);
const originalTs = first.request.ts;
await new Promise((resolve) => setTimeout(resolve, 20 ));
const second = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "operator" ,
scopes: ["operator.read" ],
},
baseDir,
);
expect(second.created).toBe(false );
expect(second.request.requestId).toBe(first.request.requestId);
expect(second.request.ts).toBe(originalTs);
});
test("supersedes pending requests when requested roles/scopes change" , async () => {
const baseDir = await makeDevicePairingDir();
const first = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "node" ,
scopes: [],
},
baseDir,
);
const second = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "operator" ,
scopes: ["operator.read" , "operator.write" ],
},
baseDir,
);
expect(second.created).toBe(true );
expect(second.request.requestId).not.toBe(first.request.requestId);
expect(second.request.role).toBe("operator" );
expect(second.request.roles).toEqual(expect.arrayContaining(["node" , "operator" ]));
expect(second.request.scopes).toEqual(
expect.arrayContaining(["operator.read" , "operator.write" ]),
);
const list = await listDevicePairing(baseDir);
expect(list.pending).toHaveLength(1 );
expect(list.pending[0 ]?.requestId).toBe(second.request.requestId);
await approveDevicePairing(
second.request.requestId,
{ callerScopes: ["operator.read" , "operator.write" ] },
baseDir,
);
const paired = await getPairedDevice("device-1" , baseDir);
expect(paired?.roles).toEqual(expect.arrayContaining(["node" , "operator" ]));
expect(paired?.scopes).toEqual(expect.arrayContaining(["operator.read" , "operator.write" ]));
});
test("approves mixed node and operator requests with admin caller scopes" , async () => {
const baseDir = await makeDevicePairingDir();
const request = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
roles: ["node" , "operator" ],
scopes: ["operator.read" , "operator.write" , "operator.talk.secrets" ],
},
baseDir,
);
await expect(
approveDevicePairing(
request.request.requestId,
{ callerScopes: ["operator.admin" , "operator.pairing" ] },
baseDir,
),
).resolves.toMatchObject({
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("preserves requested non-operator scopes on newly minted role tokens" , async () => {
const baseDir = await makeDevicePairingDir();
const request = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "node" ,
scopes: ["node.exec" ],
},
baseDir,
);
await expect(approveDevicePairing(request.request.requestId, baseDir)).resolves.toMatchObject({
status: "approved" ,
requestId: request.request.requestId,
});
const paired = await getPairedDevice("device-1" , baseDir);
expect(paired?.tokens?.node?.scopes).toEqual(["node.exec" ]);
await expect(
verifyDeviceToken({
deviceId: "device-1" ,
token: requireToken(paired?.tokens?.node?.token),
role: "node" ,
scopes: ["node.exec" ],
baseDir,
}),
).resolves.toEqual({ ok: true });
});
test.each([
{
name: "node custom scope" ,
roles: ["node" ],
scopes: ["vault.admin" ],
scope: "vault.admin" ,
callerScopes: [],
},
{
name: "operator custom scope" ,
roles: ["operator" ],
scopes: ["vault.admin" ],
scope: "vault.admin" ,
callerScopes: ["operator.pairing" ],
},
{
name: "node requesting operator scope" ,
roles: ["node" ],
scopes: ["operator.read" ],
scope: "operator.read" ,
callerScopes: ["operator.read" ],
},
])("rejects requested scopes outside requested roles: $name" , async (params) => {
const baseDir = await makeDevicePairingDir();
const request = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
roles: params.roles,
scopes: params.scopes,
},
baseDir,
);
await expect(
approveDevicePairing(
request.request.requestId,
{ callerScopes: params.callerScopes },
baseDir,
),
).resolves.toEqual({
status: "forbidden" ,
reason: "scope-outside-requested-roles" ,
scope: params.scope,
});
await expect(getPairedDevice("device-1" , baseDir)).resolves.toBeNull();
});
test("preserves existing non-operator scopes during operator-only mixed-role repairs" , async () => {
const baseDir = await makeDevicePairingDir();
const initial = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "node" ,
scopes: ["node.exec" ],
},
baseDir,
);
await expect(approveDevicePairing(initial.request.requestId, baseDir)).resolves.toMatchObject({
status: "approved" ,
requestId: initial.request.requestId,
});
const repair = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
roles: ["node" , "operator" ],
scopes: ["operator.read" ],
},
baseDir,
);
await expect(
approveDevicePairing(repair.request.requestId, { callerScopes: ["operator.read" ] }, baseDir),
).resolves.toMatchObject({
status: "approved" ,
requestId: repair.request.requestId,
});
const paired = await getPairedDevice("device-1" , baseDir);
expect(paired?.tokens?.node?.scopes).toEqual(["node.exec" ]);
expect(paired?.tokens?.operator?.scopes).toEqual(["operator.read" ]);
await expect(
verifyDeviceToken({
deviceId: "device-1" ,
token: requireToken(paired?.tokens?.node?.token),
role: "node" ,
scopes: ["node.exec" ],
baseDir,
}),
).resolves.toEqual({ ok: true });
});
test("keeps superseded requests interactive when an existing pending request is interactive" , async () => {
const baseDir = await makeDevicePairingDir();
const first = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "node" ,
scopes: [],
silent: false ,
},
baseDir,
);
expect(first.request.silent).toBe(false );
const second = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "operator" ,
scopes: ["operator.read" ],
silent: true ,
},
baseDir,
);
expect(second.created).toBe(true );
expect(second.request.requestId).not.toBe(first.request.requestId);
expect(second.request.silent).toBe(false );
});
test("rejects bootstrap token replay before pending scope escalation can be approved" , async () => {
const baseDir = await makeDevicePairingDir();
const issued = await issueDeviceBootstrapToken({
baseDir,
roles: ["operator" ],
scopes: ["operator.read" ],
});
await expect(
verifyDeviceBootstrapToken({
token: issued.token,
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "operator" ,
scopes: ["operator.read" ],
baseDir,
}),
).resolves.toEqual({ ok: true });
const first = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "operator" ,
scopes: ["operator.read" ],
},
baseDir,
);
await expect(
verifyDeviceBootstrapToken({
token: issued.token,
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "operator" ,
scopes: ["operator.admin" ],
baseDir,
}),
).resolves.toEqual({ ok: false , reason: "bootstrap_token_invalid" });
await approveDevicePairing(
first.request.requestId,
{ callerScopes: ["operator.read" ] },
baseDir,
);
const paired = await getPairedDevice("device-1" , baseDir);
expect(paired?.scopes).toEqual(["operator.read" ]);
expect(paired?.approvedScopes).toEqual(["operator.read" ]);
expect(paired?.tokens?.operator?.scopes).toEqual(["operator.read" ]);
});
test("fails closed for operator approvals when caller scopes are omitted" , async () => {
const baseDir = await makeDevicePairingDir();
const request = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "operator" ,
scopes: ["operator.admin" ],
},
baseDir,
);
await expect(approveDevicePairing(request.request.requestId, baseDir)).resolves.toEqual({
status: "forbidden" ,
reason: "caller-scopes-required" ,
scope: "operator.admin" ,
});
await expect(
approveDevicePairing(
request.request.requestId,
{
callerScopes: ["operator.admin" ],
},
baseDir,
),
).resolves.toEqual(
expect.objectContaining({
status: "approved" ,
requestId: request.request.requestId,
}),
);
});
test("metadata refresh cannot mutate approved role and scope fields" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedNodeDevice(baseDir);
await updatePairedDeviceMetadata(
"node-1" ,
{
displayName: "renamed-node" ,
role: "operator" ,
roles: ["operator" ],
scopes: ["operator.admin" ],
approvedScopes: ["operator.admin" ],
tokens: {},
publicKey: "attacker-key" ,
} as unknown as Parameters<typeof updatePairedDeviceMetadata>[1 ],
baseDir,
);
const paired = await getPairedDevice("node-1" , baseDir);
expect(paired?.displayName).toBe("renamed-node" );
expect(paired?.publicKey).toBe("public-key-node-1" );
expect(paired?.role).toBe("node" );
expect(paired?.roles).toEqual(["node" ]);
expect(paired?.scopes).toEqual([]);
expect(paired?.approvedScopes).toEqual([]);
expect(paired?.tokens?.node).toBeTruthy();
expect(paired?.tokens?.operator).toBeUndefined();
});
test("generates base64url device tokens with 256-bit entropy output length" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.admin" ]);
const paired = await getPairedDevice("device-1" , baseDir);
const token = requireToken(paired?.tokens?.operator?.token);
expect(token).toMatch(/^[A-Za-z0-9 _-]{43 }$/);
expect(Buffer.from(token, "base64url" )).toHaveLength(32 );
});
test("allows down-scoping from admin and preserves approved scope baseline" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.admin" ]);
const downscoped = await rotateDeviceToken({
deviceId: "device-1" ,
role: "operator" ,
scopes: ["operator.read" ],
baseDir,
});
expect(downscoped.ok).toBe(true );
let paired = await getPairedDevice("device-1" , baseDir);
expect(paired?.tokens?.operator?.scopes).toEqual(["operator.read" ]);
expect(paired?.scopes).toEqual(["operator.admin" ]);
expect(paired?.approvedScopes).toEqual(["operator.admin" ]);
const reused = await rotateDeviceToken({
deviceId: "device-1" ,
role: "operator" ,
baseDir,
});
expect(reused.ok).toBe(true );
paired = await getPairedDevice("device-1" , baseDir);
expect(paired?.tokens?.operator?.scopes).toEqual(["operator.read" ]);
});
test("preserves existing token scopes when approving a repair without requested scopes" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.admin" ]);
const repair = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "operator" ,
},
baseDir,
);
await approveDevicePairing(
repair.request.requestId,
{ callerScopes: ["operator.admin" ] },
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" ,
"operator.read" ,
"operator.write" ,
]);
});
test("rejects scope escalation when rotating a token and leaves state unchanged" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.read" ]);
const before = await getPairedDevice("device-1" , baseDir);
const rotated = await rotateDeviceToken({
deviceId: "device-1" ,
role: "operator" ,
scopes: ["operator.admin" ],
baseDir,
});
expect(rotated).toEqual({ ok: false , reason: "scope-outside-approved-baseline" });
const after = await getPairedDevice("device-1" , baseDir);
expect(after?.tokens?.operator?.token).toEqual(before?.tokens?.operator?.token);
expect(after?.tokens?.operator?.scopes).toEqual(["operator.read" ]);
expect(after?.scopes).toEqual(["operator.read" ]);
expect(after?.approvedScopes).toEqual(["operator.read" ]);
});
test("rejects scope escalation when ensuring a token and leaves state unchanged" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.read" ]);
const before = await getPairedDevice("device-1" , baseDir);
const ensured = await ensureDeviceToken({
deviceId: "device-1" ,
role: "operator" ,
scopes: ["operator.admin" ],
baseDir,
});
expect(ensured).toBeNull();
const after = await getPairedDevice("device-1" , baseDir);
expect(after?.tokens?.operator?.token).toEqual(before?.tokens?.operator?.token);
expect(after?.tokens?.operator?.scopes).toEqual(["operator.read" ]);
expect(after?.scopes).toEqual(["operator.read" ]);
expect(after?.approvedScopes).toEqual(["operator.read" ]);
});
test("preserves explicit empty scope baselines for node device tokens" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedNodeDevice(baseDir);
const paired = await getPairedDevice("node-1" , baseDir);
expect(paired?.scopes).toEqual([]);
expect(paired?.approvedScopes).toEqual([]);
const seededToken = requireToken(paired?.tokens?.node?.token);
await expect(
ensureDeviceToken({
deviceId: "node-1" ,
role: "node" ,
scopes: [],
baseDir,
}),
).resolves.toEqual(expect.objectContaining({ token: seededToken, scopes: [] }));
await expect(
verifyDeviceToken({
deviceId: "node-1" ,
token: seededToken,
role: "node" ,
scopes: [],
baseDir,
}),
).resolves.toEqual({ ok: true });
});
test("normalizes legacy node token scopes back to [] on re-approval" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedNodeDevice(baseDir);
await mutatePairedDevice(baseDir, "node-1" , (device) => {
const nodeToken = device.tokens?.node;
expect(nodeToken).toBeDefined();
if (!nodeToken) {
throw new Error("expected paired node token" );
}
nodeToken.scopes = ["operator.read" ];
});
const repair = await requestDevicePairing(
{
deviceId: "node-1" ,
publicKey: "public-key-node-1" ,
role: "node" ,
},
baseDir,
);
await approveDevicePairing(repair.request.requestId, { callerScopes: [] }, baseDir);
const paired = await getPairedDevice("node-1" , baseDir);
expect(paired?.scopes).toEqual([]);
expect(paired?.approvedScopes).toEqual([]);
expect(paired?.tokens?.node?.scopes).toEqual([]);
});
test("bootstrap pairing seeds node and operator device tokens explicitly" , async () => {
const baseDir = await makeDevicePairingDir();
const request = await requestDevicePairing(
{
deviceId: "bootstrap-device-1" ,
publicKey: "bootstrap-public-key-1" ,
role: "node" ,
roles: ["node" , "operator" ],
scopes: [],
silent: true ,
},
baseDir,
);
await expect(
approveBootstrapDevicePairing(
request.request.requestId,
PAIRING_SETUP_BOOTSTRAP_PROFILE,
baseDir,
),
).resolves.toEqual(expect.objectContaining({ status: "approved" }));
const paired = await getPairedDevice("bootstrap-device-1" , baseDir);
expect(paired?.roles).toEqual(expect.arrayContaining(["node" , "operator" ]));
expect(paired?.approvedScopes).toEqual(
expect.arrayContaining(PAIRING_SETUP_BOOTSTRAP_PROFILE.scopes),
);
expect(paired?.tokens?.node?.scopes).toEqual([]);
expect(paired?.tokens?.operator?.scopes).toEqual(
expect.arrayContaining(PAIRING_SETUP_BOOTSTRAP_PROFILE.scopes),
);
});
test("bootstrap pairing keeps operator token scopes operator-only" , async () => {
const baseDir = await makeDevicePairingDir();
const request = await requestDevicePairing(
{
deviceId: "bootstrap-device-operator-scope" ,
publicKey: "bootstrap-public-key-operator-scope" ,
role: "node" ,
roles: ["node" , "operator" ],
scopes: [],
silent: true ,
},
baseDir,
);
await expect(
approveBootstrapDevicePairing(
request.request.requestId,
{
roles: ["node" , "operator" ],
scopes: ["node.exec" , "operator.pairing" , "operator.read" , "operator.write" ],
},
baseDir,
),
).resolves.toEqual(expect.objectContaining({ status: "approved" }));
const paired = await getPairedDevice("bootstrap-device-operator-scope" , baseDir);
expect(paired?.tokens?.operator?.scopes).toEqual(["operator.read" , "operator.write" ]);
expect(paired?.tokens?.node?.scopes).toEqual([]);
});
test("verifies token and rejects mismatches" , async () => {
const { baseDir, token } = await setupOperatorToken(["operator.read" ]);
const ok = await verifyOperatorToken({
baseDir,
token,
scopes: ["operator.read" ],
});
expect(ok.ok).toBe(true );
const mismatch = await verifyOperatorToken({
baseDir,
token: "x" .repeat(token.length),
scopes: ["operator.read" ],
});
expect(mismatch.ok).toBe(false );
expect(mismatch.reason).toBe("token-mismatch" );
});
test("rejects persisted tokens whose scopes exceed the approved scope baseline" , async () => {
const { baseDir, token } = await setupOperatorToken(["operator.read" ]);
await overwritePairedOperatorTokenScopes(baseDir, ["operator.admin" ]);
await expect(
verifyOperatorToken({
baseDir,
token,
scopes: ["operator.admin" ],
}),
).resolves.toEqual({ ok: false , reason: "scope-mismatch" });
});
test("fails closed when the paired device approval baseline is missing during verification" , async () => {
const { baseDir, token } = await setupOperatorToken(["operator.read" ]);
await clearPairedOperatorApprovalBaseline(baseDir);
await expect(
verifyOperatorToken({
baseDir,
token,
scopes: ["operator.read" ],
}),
).resolves.toEqual({ ok: false , reason: "scope-mismatch" });
});
test("accepts operator.read/operator.write requests with an operator.admin token scope" , async () => {
const { baseDir, token } = await setupOperatorToken(["operator.admin" ]);
const readOk = await verifyOperatorToken({
baseDir,
token,
scopes: ["operator.read" ],
});
expect(readOk.ok).toBe(true );
const writeOk = await verifyOperatorToken({
baseDir,
token,
scopes: ["operator.write" ],
});
expect(writeOk.ok).toBe(true );
});
test("accepts custom operator scopes under an operator.admin approval baseline" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.admin" ]);
const rotated = await rotateDeviceToken({
deviceId: "device-1" ,
role: "operator" ,
scopes: ["operator.talk.secrets" ],
baseDir,
});
const entry = requireRotatedEntry(rotated);
expect(entry.scopes).toEqual(["operator.talk.secrets" ]);
await expect(
verifyOperatorToken({
baseDir,
token: requireToken(entry.token),
scopes: ["operator.talk.secrets" ],
}),
).resolves.toEqual({ ok: true });
});
test("fails closed when the paired device approval baseline is missing during ensure" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.admin" ]);
await clearPairedOperatorApprovalBaseline(baseDir);
await expect(
ensureDeviceToken({
deviceId: "device-1" ,
role: "operator" ,
scopes: ["operator.admin" ],
baseDir,
}),
).resolves.toBeNull();
});
test("fails closed when the paired device approval baseline is missing during rotation" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.admin" ]);
await clearPairedOperatorApprovalBaseline(baseDir);
await expect(
rotateDeviceToken({
deviceId: "device-1" ,
role: "operator" ,
scopes: ["operator.admin" ],
baseDir,
}),
).resolves.toEqual({ ok: false , reason: "missing-approved-scope-baseline" });
});
test("treats multibyte same-length token input as mismatch without throwing" , async () => {
const { baseDir, token } = await setupOperatorToken(["operator.read" ]);
const multibyteToken = "é" .repeat(token.length);
expect(Buffer.from(multibyteToken).length).not.toBe(Buffer.from(token).length);
await expect(
verifyOperatorToken({
baseDir,
token: multibyteToken,
scopes: ["operator.read" ],
}),
).resolves.toEqual({ ok: false , reason: "token-mismatch" });
});
test("derives effective roles from active tokens instead of sticky historical roles" , async () => {
const baseDir = await makeDevicePairingDir();
const request = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1" ,
role: "node" ,
},
baseDir,
);
await approveDevicePairing(request.request.requestId, { callerScopes: [] }, baseDir);
let paired = await getPairedDevice("device-1" , baseDir);
expect(paired).toBeDefined();
if (!paired) {
throw new Error("expected paired node device" );
}
expect(paired?.roles).toContain("node" );
expect(listEffectivePairedDeviceRoles(paired)).toEqual(["node" ]);
expect(hasEffectivePairedDeviceRole(paired, "node" )).toBe(true );
await revokeDeviceToken({ deviceId: "device-1" , role: "node" , baseDir });
paired = await getPairedDevice("device-1" , baseDir);
expect(paired).toBeDefined();
if (!paired) {
throw new Error("expected paired node device after revoke" );
}
expect(paired?.roles).toContain("node" );
expect(listEffectivePairedDeviceRoles(paired)).toEqual([]);
expect(hasEffectivePairedDeviceRole(paired, "node" )).toBe(false );
});
test("fails closed for tokenless legacy role fields" , async () => {
const device: PairedDevice = {
deviceId: "device-fallback" ,
publicKey: "pk-fallback" ,
role: "node" ,
roles: ["node" , "operator" ],
tokens: {},
createdAtMs: Date.now(),
approvedAtMs: Date.now(),
};
expect(listEffectivePairedDeviceRoles(device)).toEqual([]);
expect(hasEffectivePairedDeviceRole(device, "node" )).toBe(false );
expect(hasEffectivePairedDeviceRole(device, "operator" )).toBe(false );
});
test("filters active token roles to the approved pairing role set" , async () => {
const now = Date.now();
const device: PairedDevice = {
deviceId: "device-filtered" ,
publicKey: "pk-filtered" ,
role: "operator" ,
roles: ["operator" ],
tokens: {
node: {
token: "forged-node-token" ,
role: "node" ,
scopes: [],
createdAtMs: now,
},
operator: {
token: "real-operator-token" ,
role: "operator" ,
scopes: ["operator.read" ],
createdAtMs: now,
},
},
createdAtMs: now,
approvedAtMs: now,
};
expect(listEffectivePairedDeviceRoles(device)).toEqual(["operator" ]);
expect(hasEffectivePairedDeviceRole(device, "node" )).toBe(false );
});
test("rejects rotating a token for a role that was never approved" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.pairing" ]);
await expect(
rotateDeviceToken({
deviceId: "device-1" ,
role: "node" ,
baseDir,
}),
).resolves.toEqual({ ok: false , reason: "unknown-device-or-role" });
const paired = await getPairedDevice("device-1" , baseDir);
expect(paired?.tokens?.node).toBeUndefined();
expect(paired && listEffectivePairedDeviceRoles(paired)).toEqual(["operator" ]);
});
test("removes paired devices by device id" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.read" ]);
const removed = await removePairedDevice("device-1" , baseDir);
expect(removed).toEqual({ deviceId: "device-1" });
await expect(getPairedDevice("device-1" , baseDir)).resolves.toBeNull();
await expect(removePairedDevice("device-1" , baseDir)).resolves.toBeNull();
});
test("removing a paired device clears pending requests for that device only" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.read" ]);
const staleRepair = await requestDevicePairing(
{
deviceId: "device-1" ,
publicKey: "public-key-1-rotated" ,
role: "operator" ,
scopes: ["operator.read" ],
},
baseDir,
);
const otherPending = await requestDevicePairing(
{
deviceId: "device-2" ,
publicKey: "public-key-2" ,
role: "node" ,
scopes: [],
},
baseDir,
);
await expect(removePairedDevice("device-1" , baseDir)).resolves.toEqual({
deviceId: "device-1" ,
});
const pending = (await listDevicePairing(baseDir)).pending;
expect(pending.map((entry) => entry.requestId)).not.toContain(staleRepair.request.requestId);
expect(pending.map((entry) => entry.requestId)).toContain(otherPending.request.requestId);
await expect(
approveDevicePairing(
staleRepair.request.requestId,
{ callerScopes: ["operator.read" ] },
baseDir,
),
).resolves.toBeNull();
await expect(getPairedDevice("device-1" , baseDir)).resolves.toBeNull();
});
test("clears paired device state by device id" , async () => {
const baseDir = await makeDevicePairingDir();
await setupPairedOperatorDevice(baseDir, ["operator.read" ]);
await expect(clearDevicePairing("device-1" , baseDir)).resolves.toBe(true );
await expect(getPairedDevice("device-1" , baseDir)).resolves.toBeNull();
await expect(clearDevicePairing("device-1" , baseDir)).resolves.toBe(false );
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.14 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland