import { chmod, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; export type KuberSession = { token: string; expiresAt: string; user: { username: string; roles: string[]; }; }; let cachedSession: | { runtimePath: string; persistentPath: string; value: KuberSession | undefined; expiresAt: number; } | undefined; function runtimeSessionPath(): string { const directory = process.env.XDG_RUNTIME_DIR ?? join(tmpdir(), `kuber-${process.getuid?.() ?? "user"}`); return join(directory, "kuber", "session.json"); } function persistentSessionPath(): string { const directory = process.env.XDG_CONFIG_HOME ?? join(process.env.HOME ?? tmpdir(), ".config"); return join(directory, "kuber", "session.json"); } export function getSessionPath(persistent: boolean): string { return persistent ? persistentSessionPath() : runtimeSessionPath(); } function isSession(value: unknown): value is KuberSession { if (!value || typeof value !== "object") return false; const session = value as Partial; return ( typeof session.token === "string" && typeof session.expiresAt === "string" && Boolean(session.user) && typeof session.user?.username === "string" && Array.isArray(session.user.roles) && session.user.roles.every((role) => typeof role === "string") ); } async function readSessionFile( path: string, ): Promise { try { const value: unknown = JSON.parse(await readFile(path, "utf8")); if (!isSession(value)) return; if (Date.parse(value.expiresAt) <= Date.now()) { await rm(path, { force: true }); return; } return value; } catch (error) { if ( error && typeof error === "object" && "code" in error && error.code === "ENOENT" ) { return; } if (error instanceof SyntaxError) return; throw error; } } export async function readSession(): Promise { const runtimePath = runtimeSessionPath(); const persistentPath = persistentSessionPath(); if ( cachedSession?.runtimePath === runtimePath && cachedSession.persistentPath === persistentPath && (cachedSession.expiresAt === 0 || cachedSession.expiresAt > Date.now()) ) return cachedSession.value; const value = (await readSessionFile(runtimePath)) ?? (await readSessionFile(persistentPath)); cachedSession = { runtimePath, persistentPath, value, // Expiration is checked on every lookup, even while the file is memoized. expiresAt: value ? Date.parse(value.expiresAt) : 0, }; return value; } export async function writeSession( session: KuberSession, persistent: boolean, ): Promise { const path = getSessionPath(persistent); await mkdir(dirname(path), { recursive: true, mode: 0o700 }); await chmod(dirname(path), 0o700); const temporaryPath = `${path}.${randomUUID()}.tmp`; await writeFile(temporaryPath, `${JSON.stringify(session, null, 2)}\n`, { flag: "wx", mode: 0o600, }); await rename(temporaryPath, path); await chmod(path, 0o600); await rm(getSessionPath(!persistent), { force: true }); cachedSession = undefined; return path; } export async function removeSessions(): Promise { await Promise.all( [runtimeSessionPath(), persistentSessionPath()].map((path) => rm(path, { force: true }), ), ); cachedSession = undefined; }