Separate YouTube video titles from on-video song/artist fields with Pro gating, serve curated fonts and watermark assets for 1:1 preview parity, and include billing/API/docs/deploy stack for self-hosted release.
45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
/** Server-only font filesystem helpers. Do not import from client components. */
|
|
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { CURATED_FONTS, type CuratedFontKey } from "./fonts";
|
|
|
|
export function getFontsDir(): string {
|
|
return path.join(process.cwd(), "assets", "fonts");
|
|
}
|
|
|
|
/** Resolve a curated font file on disk (must exist under assets/fonts). */
|
|
export function resolveCuratedFontPath(key: CuratedFontKey): string {
|
|
const meta = CURATED_FONTS.find((f) => f.key === key);
|
|
if (!meta) throw new Error("Unknown font");
|
|
// Whitelist filename only never accept user-controlled path segments
|
|
const safe = meta.file.replace(/[^a-zA-Z0-9._-]/g, "");
|
|
if (safe !== meta.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();
|
|
}
|
|
}
|