/** Server-only font filesystem helpers. Do not import from client components. */ import fs from "fs/promises"; import path from "path"; import { CURATED_FONTS, SYSTEM_FONT, type CuratedFontKey } from "./fonts"; export function getFontsDir(): string { return path.join(process.cwd(), "assets", "fonts"); } /** Resolve the bundled system (Arimo) font for FFmpeg + preview. */ export function resolveSystemFontPath(weight: "regular" | "bold" = "regular"): string { const file = weight === "bold" ? SYSTEM_FONT.boldFile : SYSTEM_FONT.file; const safe = file.replace(/[^a-zA-Z0-9._-]/g, ""); if (safe !== file) throw new Error("Invalid font asset name"); return path.join(getFontsDir(), safe); } /** Resolve a curated font file on disk (must exist under assets/fonts). */ export function resolveCuratedFontPath( key: CuratedFontKey, weight: "regular" | "bold" = "regular", ): string { const meta = CURATED_FONTS.find((f) => f.key === key); if (!meta) throw new Error("Unknown font"); const file = weight === "bold" && meta.boldFile ? meta.boldFile : meta.file; // Whitelist filename only never accept user-controlled path segments const safe = file.replace(/[^a-zA-Z0-9._-]/g, ""); if (safe !== file) throw new Error("Invalid font asset name"); return path.join(getFontsDir(), safe); } /** * Validate TTF / OTF magic bytes. * TTF: 00 01 00 00 | true | typ1 * OTF: OTTO */ export async function assertFontFile(filePath: string): Promise<"ttf" | "otf"> { const fh = await fs.open(filePath, "r"); try { const buf = Buffer.alloc(4); await fh.read(buf, 0, 4, 0); const asStr = buf.toString("ascii"); if (asStr === "OTTO") return "otf"; if (asStr === "true" || asStr === "typ1") return "ttf"; if (buf[0] === 0x00 && buf[1] === 0x01 && buf[2] === 0x00 && buf[3] === 0x00) { return "ttf"; } if (asStr === "wOFF" || asStr === "wOF2") { throw new Error("WOFF fonts are not supported. Upload a .ttf or .otf file."); } throw new Error("File is not a valid TTF or OTF font"); } finally { await fh.close(); } }