Files
openclaw/ui/src/ui/controllers/devices.ts
2026-02-03 22:45:29 +09:00

160 lines
4.0 KiB
TypeScript

import type { GatewayBrowserClient } from "../gateway";
import { clearDeviceAuthToken, storeDeviceAuthToken } from "../device-auth";
import { loadOrCreateDeviceIdentity } from "../device-identity";
export type DeviceTokenSummary = {
role: string;
scopes?: string[];
createdAtMs?: number;
rotatedAtMs?: number;
revokedAtMs?: number;
lastUsedAtMs?: number;
};
export type PendingDevice = {
requestId: string;
deviceId: string;
displayName?: string;
role?: string;
remoteIp?: string;
isRepair?: boolean;
ts?: number;
};
export type PairedDevice = {
deviceId: string;
displayName?: string;
roles?: string[];
scopes?: string[];
remoteIp?: string;
tokens?: DeviceTokenSummary[];
createdAtMs?: number;
approvedAtMs?: number;
};
export type DevicePairingList = {
pending: PendingDevice[];
paired: PairedDevice[];
};
export type DevicesState = {
client: GatewayBrowserClient | null;
connected: boolean;
devicesLoading: boolean;
devicesError: string | null;
devicesList: DevicePairingList | null;
};
export async function loadDevices(state: DevicesState, opts?: { quiet?: boolean }) {
if (!state.client || !state.connected) {
return;
}
if (state.devicesLoading) {
return;
}
state.devicesLoading = true;
if (!opts?.quiet) {
state.devicesError = null;
}
try {
const res = await state.client.request<{
pending?: Array<PendingDevice>;
paired?: Array<PendingDevice>;
}>("device.pair.list", {});
state.devicesList = {
pending: Array.isArray(res?.pending) ? res.pending : [],
paired: Array.isArray(res?.paired) ? res.paired : [],
};
} catch (err) {
if (!opts?.quiet) {
state.devicesError = String(err);
}
} finally {
state.devicesLoading = false;
}
}
export async function approveDevicePairing(state: DevicesState, requestId: string) {
if (!state.client || !state.connected) {
return;
}
try {
await state.client.request("device.pair.approve", { requestId });
await loadDevices(state);
} catch (err) {
state.devicesError = String(err);
}
}
export async function rejectDevicePairing(state: DevicesState, requestId: string) {
if (!state.client || !state.connected) {
return;
}
const confirmed = window.confirm("Reject this device pairing request?");
if (!confirmed) {
return;
}
try {
await state.client.request("device.pair.reject", { requestId });
await loadDevices(state);
} catch (err) {
state.devicesError = String(err);
}
}
export async function rotateDeviceToken(
state: DevicesState,
params: { deviceId: string; role: string; scopes?: string[] },
) {
if (!state.client || !state.connected) {
return;
}
try {
const res = await state.client.request<{
token: string;
role?: string;
deviceId?: string;
scopes?: Array<string>;
}>("device.token.rotate", params);
if (res?.token) {
const identity = await loadOrCreateDeviceIdentity();
const role = res.role ?? params.role;
if (res.deviceId === identity.deviceId || params.deviceId === identity.deviceId) {
storeDeviceAuthToken({
deviceId: identity.deviceId,
role,
token: res.token,
scopes: res.scopes ?? params.scopes ?? [],
});
}
window.prompt("New device token (copy and store securely):", res.token);
}
await loadDevices(state);
} catch (err) {
state.devicesError = String(err);
}
}
export async function revokeDeviceToken(
state: DevicesState,
params: { deviceId: string; role: string },
) {
if (!state.client || !state.connected) {
return;
}
const confirmed = window.confirm(`Revoke token for ${params.deviceId} (${params.role})?`);
if (!confirmed) {
return;
}
try {
await state.client.request("device.token.revoke", params);
const identity = await loadOrCreateDeviceIdentity();
if (params.deviceId === identity.deviceId) {
clearDeviceAuthToken({ deviceId: identity.deviceId, role: params.role });
}
await loadDevices(state);
} catch (err) {
state.devicesError = String(err);
}
}