import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { randomUUID } from "node:crypto"; export interface PrincipalContext { issuer: string; subject: string; displayName?: string; isAdmin: boolean; } const principalEnvKeys = [ "THT_PRINCIPAL_ISSUER", "THT_PRINCIPAL_SUBJECT", "THT_PRINCIPAL_DISPLAY_NAME", "THT_PRINCIPAL_IS_ADMIN", ] as const; export function clearPrincipalEnvironment(env: NodeJS.ProcessEnv): void { for (const key of principalEnvKeys) delete env[key]; } export function expandLocalHome(path: string, home = homedir()): string { if (path === "~") return home; if (path.startsWith("~/")) return join(home, path.slice(2)); return path; } function harden(path: string, mode: number): void { if (process.platform === "win32") return; try { chmodSync(path, mode); } catch { /* best-effort parity with harness local storage */ } } const invalid = (value: string) => value.length === 0 || value.length > 512 || /[\u0000-\u001f\u007f]/.test(value); function required(value: unknown): string | undefined { if (typeof value !== "string") return undefined; const normalized = value.trim(); return invalid(normalized) ? undefined : normalized; } function optional(value: unknown): string | undefined { if (value === undefined) return undefined; return required(value); } export function upstreamPrincipal(headers: Record): PrincipalContext | undefined { const issuer = required(headers["x-thoth-principal-issuer"]); const subject = required(headers["x-thoth-principal-subject"]); const displayName = optional(headers["x-thoth-principal-display-name"]); const adminHeader = headers["x-thoth-is-admin"]; if (!issuer || !subject || (headers["x-thoth-principal-display-name"] !== undefined && !displayName)) return undefined; if (adminHeader !== "0" && adminHeader !== "1" && adminHeader !== "true" && adminHeader !== "false") return undefined; return { issuer, subject, displayName, isAdmin: adminHeader === "1" || adminHeader === "true" }; } export function localPrincipal(): PrincipalContext { const home = expandLocalHome(process.env.THT_HOME ?? join(homedir(), ".thothii")); const identityPath = join(home, "identity.json"); mkdirSync(home, { recursive: true, mode: 0o700 }); harden(home, 0o700); try { const stored = JSON.parse(readFileSync(identityPath, "utf8")); if (stored?.issuer === "local" && typeof stored.subject === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(stored.subject)) { harden(identityPath, 0o600); return { issuer: "local", subject: stored.subject, isAdmin: false }; } throw new Error("invalid local identity"); } catch (error: any) { if (error?.code !== "ENOENT") throw error; const principal = { issuer: "local", subject: randomUUID() }; try { writeFileSync(identityPath, JSON.stringify(principal) + "\n", { mode: 0o600, flag: "wx" }); harden(identityPath, 0o600); return { ...principal, isAdmin: false }; } catch (writeError: any) { // Another local request won the identity creation race; always converge on its UUID. if (writeError?.code === "EEXIST") return localPrincipal(); throw writeError; } } } export function principalEnvironment(principal: PrincipalContext): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { THT_PRINCIPAL_ISSUER: principal.issuer, THT_PRINCIPAL_SUBJECT: principal.subject, THT_PRINCIPAL_IS_ADMIN: principal.isAdmin ? "true" : "false", }; if (principal.displayName) env.THT_PRINCIPAL_DISPLAY_NAME = principal.displayName; return env; }