// Shared password gate for the library writes that carry the SAME primitive `hooks.put.ts` gates. // // A custom library entry can carry `prep` (commands run before the title launches) and a // `launch.kind === "command"` (a shell command run at launch). Both are executed verbatim as the // host user — `/bin/sh -c` on Linux, `cmd.exe /c` on Windows — which is the very thing the hooks // gate exists to stop a bare session cookie from doing: *"a 7-day session cookie must not be enough // to leave a persistent command behind on the machine."* // // `confirm.ts` gated three routes and these were not among them, so the identical primitive fell // through the ungated `/api/**` catch-all where the BFF attaches the admin bearer unconditionally // (2026-08-05 review M-6). Anyone with a session cookie but not the password — a borrowed browser, // an exfiltrated cookie, a stale 7-day session after a password rotation — could leave a command // behind. `SameSite=lax` blocks a plain cross-site POST, so this is cookie possession rather than // drive-by CSRF, but the invariant is the same one. // // The gate is CONDITIONAL on the payload actually carrying one of those fields. An ordinary library // edit — title, artwork, platform, a `steam_appid` launch — is not code execution and prompting for // it would only train the operator to type their password without reading it. Same reasoning as // "a catalog install from an already-trusted source is deliberately NOT gated" in `confirm.ts`. import type { H3Event } from "h3"; import { confirmPassword } from "./confirm"; /** The shape the gate inspects; everything else about the entry is none of its business. */ interface EntryLike { prep?: unknown; launch?: { kind?: unknown } | null; } /** Does this entry carry a field the host will hand to a shell? */ export function carriesCommandExecution(entry: EntryLike | null | undefined): boolean { if (!entry || typeof entry !== "object") return false; if (Array.isArray(entry.prep) && entry.prep.length > 0) return true; return entry.launch?.kind === "command"; } /** * Re-verify the console password iff `entries` contains a command-execution field. Throws the same * 401/429/503 `confirmPassword` does; returns normally when the gate does not apply. */ export function confirmIfCommandExecution( event: H3Event, entries: EntryLike | EntryLike[] | null | undefined, password: unknown, ): void { const list = Array.isArray(entries) ? entries : [entries]; if (list.some(carriesCommandExecution)) confirmPassword(event, password); }