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; }; 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(); 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; } export function clearDeviceAuthToken(params: { deviceId: string; role: string; env?: NodeJS.ProcessEnv; }): void { const filePath = resolveDeviceAuthPath(params.env); const store = readStore(filePath); if (!store || store.deviceId !== params.deviceId) return; const role = normalizeRole(params.role); if (!store.tokens[role]) return; const next: DeviceAuthStore = { version: 1, deviceId: store.deviceId, tokens: { ...store.tokens }, }; delete next.tokens[role]; writeStore(filePath, next); }