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:
parent
9f8b2cc987
commit
74fc22ddfb
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue