pi-remote-control/extensions/remote-control/auth.ts

66 lines
1.9 KiB
TypeScript

/**
* Authentication helpers for remote-control.
*
* Provides one-time token generation/validation and session cookie management.
*/
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { randomBytes, timingSafeEqual } from "node:crypto";
export function generateToken(): string {
return randomBytes(24).toString("base64url"); // 32 chars, URL-safe
}
const TOKEN_FILE = path.join(os.homedir(), ".pi", "remote-control", "token");
/** Load persisted token, or generate + save a new one. */
export async function loadOrCreateToken(): Promise<string> {
try {
const token = (await fs.readFile(TOKEN_FILE, "utf8")).trim();
if (token.length > 0) return token;
} catch {
/* file doesn't exist yet */
}
const token = generateToken();
await fs.mkdir(path.dirname(TOKEN_FILE), { recursive: true });
await fs.writeFile(TOKEN_FILE, token, { encoding: "utf8", mode: 0o600 });
return token;
}
export function validateToken(provided: string, expected: string): boolean {
const a = Buffer.from(provided);
const b = Buffer.from(expected);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
/** Name of the cookie that grants access after initial token validation */
export const SESSION_COOKIE = "pi_rc_session";
export function generateSessionId(): string {
return randomBytes(24).toString("base64url");
}
export function parseCookies(
header: string | undefined,
): Record<string, string> {
const cookies: Record<string, string> = {};
if (!header) return cookies;
for (const pair of header.split(";")) {
const idx = pair.indexOf("=");
if (idx < 0) continue;
const name = pair.slice(0, idx).trim();
const raw = pair.slice(idx + 1).trim();
let value = raw;
try {
value = decodeURIComponent(raw);
} catch {
/* keep raw */
}
cookies[name] = value;
}
return cookies;
}