Spaces:
Running
Running
| 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].toSorted(); | |
| } | |
| 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); | |
| } | |