137 lines
3.5 KiB
TypeScript
137 lines
3.5 KiB
TypeScript
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<KuberSession>;
|
|
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<KuberSession | undefined> {
|
|
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<KuberSession | undefined> {
|
|
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<string> {
|
|
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<void> {
|
|
await Promise.all(
|
|
[runtimeSessionPath(), persistentSessionPath()].map((path) =>
|
|
rm(path, { force: true }),
|
|
),
|
|
);
|
|
cachedSession = undefined;
|
|
}
|