feat: persist auth token across server restarts

Token is stored in ~/.pi/remote-control/token (mode 600) on first start
and reused on subsequent starts — saved URLs stay valid indefinitely.
This commit is contained in:
jay 2026-05-14 19:00:31 +02:00
parent 9f8b2cc987
commit 74fc22ddfb
2 changed files with 21 additions and 2 deletions

View File

@ -4,12 +4,31 @@
* Provides one-time token generation/validation and session cookie management. * 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"; import { randomBytes, timingSafeEqual } from "node:crypto";
export function generateToken(): string { export function generateToken(): string {
return randomBytes(24).toString("base64url"); // 32 chars, URL-safe 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 { export function validateToken(provided: string, expected: string): boolean {
const a = Buffer.from(provided); const a = Buffer.from(provided);
const b = Buffer.from(expected); const b = Buffer.from(expected);

View File

@ -16,7 +16,7 @@ import type {
} from "@earendil-works/pi-coding-agent"; } from "@earendil-works/pi-coding-agent";
import { import {
generateSessionId, generateSessionId,
generateToken, loadOrCreateToken,
parseCookies, parseCookies,
SESSION_COOKIE, SESSION_COOKIE,
validateToken, validateToken,
@ -75,7 +75,7 @@ export async function startServer(
: { host: "127.0.0.1", port: 0 }; : { host: "127.0.0.1", port: 0 };
const clientChangeListeners: Array<() => void> = []; const clientChangeListeners: Array<() => void> = [];
const clients = new Set<WsClient>(); const clients = new Set<WsClient>();
const token = generateToken(); const token = await loadOrCreateToken();
// Map of valid session IDs → expiry timestamp (ms since epoch) // Map of valid session IDs → expiry timestamp (ms since epoch)
const SESSION_TTL_MS = 86_400_000; // 24 h — matches cookie Max-Age const SESSION_TTL_MS = 86_400_000; // 24 h — matches cookie Max-Age
const validSessions = new Map<string, number>(); const validSessions = new Map<string, number>();