Files
penguin-harness/packages/server/src/services/desktop-service.ts
T
2026-08-07 19:02:48 +08:00

55 lines
2.0 KiB
TypeScript

/**
* Desktop mode (PENGUIN_DESKTOP_TOKEN): the shell that spawned this server proves itself
* with a per-launch random token, which backs two endpoints with different consumption
* rules:
*
* - `GET /api/auth/desktop-login?token=…` — ONE-SHOT: the window's first navigation
* redeems the token for a standard admin cookie session; every later attempt fails,
* so a leaked URL cannot be replayed.
* - `POST /api/desktop/shutdown` (Authorization: Bearer <token>) — REUSABLE for the
* process lifetime: the token here identifies the supervising shell, which may need
* the endpoint at any point (POSIX quit, and the only graceful path on Windows,
* where killing a child is a hard TerminateProcess).
*
* Comparisons hash both sides first so timingSafeEqual gets equal-length buffers.
*/
import { createHash, timingSafeEqual } from "node:crypto";
function digest(value: string): Buffer {
return createHash("sha256").update(value).digest();
}
export class DesktopService {
private readonly tokenDigest: Buffer;
private loginConsumed = false;
private shutdownHandler: (() => void) | null = null;
constructor(token: string) {
this.tokenDigest = digest(token);
}
/** Constant-time token check (no consumption). */
verifyToken(candidate: string): boolean {
return timingSafeEqual(digest(candidate), this.tokenDigest);
}
/** One-shot login redemption: true exactly once, for the correct token. */
redeemLoginToken(candidate: string): boolean {
if (this.loginConsumed || !this.verifyToken(candidate)) return false;
this.loginConsumed = true;
return true;
}
/** index.ts registers the actual graceful-shutdown trigger after assembly. */
onShutdownRequest(handler: () => void): void {
this.shutdownHandler = handler;
}
/** Invoked by the shutdown route; false when no handler is registered (tests). */
requestShutdown(): boolean {
if (!this.shutdownHandler) return false;
this.shutdownHandler();
return true;
}
}