mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-23 15:11:42 +00:00
* Pairing: require caller scopes on approvals * Gateway: reject forbidden silent pairing results
124 lines
3.3 KiB
TypeScript
124 lines
3.3 KiB
TypeScript
import os from "node:os";
|
|
import path from "node:path";
|
|
import { expect } from "vitest";
|
|
import { WebSocket } from "ws";
|
|
import {
|
|
loadOrCreateDeviceIdentity,
|
|
publicKeyRawBase64UrlFromPem,
|
|
type DeviceIdentity,
|
|
} from "../infra/device-identity.js";
|
|
import {
|
|
approveDevicePairing,
|
|
getPairedDevice,
|
|
requestDevicePairing,
|
|
rotateDeviceToken,
|
|
} from "../infra/device-pairing.js";
|
|
import { trackConnectChallengeNonce } from "./test-helpers.js";
|
|
|
|
export function resolveDeviceIdentityPath(name: string): string {
|
|
const root = process.env.OPENCLAW_STATE_DIR ?? process.env.HOME ?? os.tmpdir();
|
|
return path.join(root, "test-device-identities", `${name}.json`);
|
|
}
|
|
|
|
export function loadDeviceIdentity(name: string): {
|
|
identityPath: string;
|
|
identity: DeviceIdentity;
|
|
publicKey: string;
|
|
} {
|
|
const identityPath = resolveDeviceIdentityPath(name);
|
|
const identity = loadOrCreateDeviceIdentity(identityPath);
|
|
return {
|
|
identityPath,
|
|
identity,
|
|
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
|
|
};
|
|
}
|
|
|
|
export async function pairDeviceIdentity(params: {
|
|
name: string;
|
|
role: "node" | "operator";
|
|
scopes: string[];
|
|
clientId?: string;
|
|
clientMode?: string;
|
|
}): Promise<{
|
|
identityPath: string;
|
|
identity: DeviceIdentity;
|
|
publicKey: string;
|
|
}> {
|
|
const loaded = loadDeviceIdentity(params.name);
|
|
const request = await requestDevicePairing({
|
|
deviceId: loaded.identity.deviceId,
|
|
publicKey: loaded.publicKey,
|
|
role: params.role,
|
|
scopes: params.scopes,
|
|
clientId: params.clientId,
|
|
clientMode: params.clientMode,
|
|
});
|
|
await approveDevicePairing(request.request.requestId, {
|
|
callerScopes: params.scopes,
|
|
});
|
|
return loaded;
|
|
}
|
|
|
|
export async function issueOperatorToken(params: {
|
|
name: string;
|
|
approvedScopes: string[];
|
|
tokenScopes?: string[];
|
|
clientId?: string;
|
|
clientMode?: string;
|
|
}): Promise<{
|
|
deviceId: string;
|
|
identityPath: string;
|
|
token: string;
|
|
}> {
|
|
const paired = await pairDeviceIdentity({
|
|
name: params.name,
|
|
role: "operator",
|
|
scopes: params.approvedScopes,
|
|
clientId: params.clientId,
|
|
clientMode: params.clientMode,
|
|
});
|
|
if (params.tokenScopes) {
|
|
const rotated = await rotateDeviceToken({
|
|
deviceId: paired.identity.deviceId,
|
|
role: "operator",
|
|
scopes: params.tokenScopes,
|
|
});
|
|
expect(rotated.ok).toBe(true);
|
|
const token = rotated.ok ? rotated.entry.token : "";
|
|
expect(token).toBeTruthy();
|
|
return {
|
|
deviceId: paired.identity.deviceId,
|
|
identityPath: paired.identityPath,
|
|
token,
|
|
};
|
|
}
|
|
|
|
const device = await getPairedDevice(paired.identity.deviceId);
|
|
const token = device?.tokens?.operator?.token ?? "";
|
|
expect(token).toBeTruthy();
|
|
expect(device?.approvedScopes).toEqual(params.approvedScopes);
|
|
return {
|
|
deviceId: paired.identity.deviceId,
|
|
identityPath: paired.identityPath,
|
|
token,
|
|
};
|
|
}
|
|
|
|
export async function openTrackedWs(port: number): Promise<WebSocket> {
|
|
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
|
trackConnectChallengeNonce(ws);
|
|
await new Promise<void>((resolve, reject) => {
|
|
const timer = setTimeout(() => reject(new Error("timeout waiting for ws open")), 5_000);
|
|
ws.once("open", () => {
|
|
clearTimeout(timer);
|
|
resolve();
|
|
});
|
|
ws.once("error", (error) => {
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
});
|
|
});
|
|
return ws;
|
|
}
|