169c7d5006
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
55 lines
2.0 KiB
TypeScript
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;
|
|
}
|
|
}
|