mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:08:27 +00:00
feat: add device token auth and devices cli
This commit is contained in:
104
src/infra/device-auth-store.ts
Normal file
104
src/infra/device-auth-store.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
|
||||
export type DeviceAuthEntry = {
|
||||
token: string;
|
||||
role: string;
|
||||
scopes: string[];
|
||||
updatedAtMs: number;
|
||||
};
|
||||
|
||||
type DeviceAuthStore = {
|
||||
version: 1;
|
||||
deviceId: string;
|
||||
tokens: Record<string, DeviceAuthEntry>;
|
||||
};
|
||||
|
||||
const DEVICE_AUTH_FILE = "device-auth.json";
|
||||
|
||||
function resolveDeviceAuthPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
return path.join(resolveStateDir(env), "identity", DEVICE_AUTH_FILE);
|
||||
}
|
||||
|
||||
function normalizeRole(role: string): string {
|
||||
return role.trim();
|
||||
}
|
||||
|
||||
function normalizeScopes(scopes: string[] | undefined): string[] {
|
||||
if (!Array.isArray(scopes)) return [];
|
||||
const out = new Set<string>();
|
||||
for (const scope of scopes) {
|
||||
const trimmed = scope.trim();
|
||||
if (trimmed) out.add(trimmed);
|
||||
}
|
||||
return [...out].sort();
|
||||
}
|
||||
|
||||
function readStore(filePath: string): DeviceAuthStore | null {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as DeviceAuthStore;
|
||||
if (parsed?.version !== 1 || typeof parsed.deviceId !== "string") return null;
|
||||
if (!parsed.tokens || typeof parsed.tokens !== "object") return null;
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(filePath: string, store: DeviceAuthStore): void {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
|
||||
try {
|
||||
fs.chmodSync(filePath, 0o600);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
export function loadDeviceAuthToken(params: {
|
||||
deviceId: string;
|
||||
role: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): DeviceAuthEntry | null {
|
||||
const filePath = resolveDeviceAuthPath(params.env);
|
||||
const store = readStore(filePath);
|
||||
if (!store) return null;
|
||||
if (store.deviceId !== params.deviceId) return null;
|
||||
const role = normalizeRole(params.role);
|
||||
const entry = store.tokens[role];
|
||||
if (!entry || typeof entry.token !== "string") return null;
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function storeDeviceAuthToken(params: {
|
||||
deviceId: string;
|
||||
role: string;
|
||||
token: string;
|
||||
scopes?: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): DeviceAuthEntry {
|
||||
const filePath = resolveDeviceAuthPath(params.env);
|
||||
const existing = readStore(filePath);
|
||||
const role = normalizeRole(params.role);
|
||||
const next: DeviceAuthStore = {
|
||||
version: 1,
|
||||
deviceId: params.deviceId,
|
||||
tokens:
|
||||
existing && existing.deviceId === params.deviceId && existing.tokens
|
||||
? { ...existing.tokens }
|
||||
: {},
|
||||
};
|
||||
const entry: DeviceAuthEntry = {
|
||||
token: params.token,
|
||||
role,
|
||||
scopes: normalizeScopes(params.scopes),
|
||||
updatedAtMs: Date.now(),
|
||||
};
|
||||
next.tokens[role] = entry;
|
||||
writeStore(filePath, next);
|
||||
return entry;
|
||||
}
|
||||
Reference in New Issue
Block a user