import type { ErrorCode } from "./error"; import { normalizeProjectAssetPath } from "./asset-path"; import type { StorageAssetPutResult, StorageAssetReadResult } from "./storage"; export const EXTERNAL_VFONT_SCHEMA_VERSION = 1 as const; export const EXTERNAL_VFONT_MAX_BYTES = 32 * 1024 * 1024; export type ExternalVFontFormat = "TTF" | "OTF" | "PFB"; export interface ExternalVFontImportRequestIR { sourcePath: string; mimeType: string; byteLength: number; sha256: string; data: ArrayBuffer; } export interface ValidatedExternalVFontIR { schemaVersion: typeof EXTERNAL_VFONT_SCHEMA_VERSION; sourcePath: string; fileName: string; format: ExternalVFontFormat; mimeType: string; byteLength: number; sha256: string; data: ArrayBuffer; } export interface ExternalVFontMainImportProofIR { schemaVersion: typeof EXTERNAL_VFONT_SCHEMA_VERSION; projectId: string; assetId: string; assetPath: string; sourcePath: string; name: string; format: ExternalVFontFormat; mimeType: string; byteLength: number; sha256: string; } export interface ExternalVFontMainImportIR extends ExternalVFontMainImportProofIR { data: ArrayBuffer; } export class ExternalVFontValidationError extends Error { constructor(readonly code: ErrorCode, message: string) { super(`${code}: ${message}`); this.name = "ExternalVFontValidationError"; } } const SHA256 = /^[0-9a-f]{64}$/; const PROJECT_ID = /^[A-Za-z0-9_-]{1,64}$/; const FORMAT = { ".ttf": { format: "TTF", mimeTypes: new Set(["font/ttf", "application/x-font-ttf", "application/font-sfnt"]) }, ".otf": { format: "OTF", mimeTypes: new Set(["font/otf", "application/vnd.ms-opentype", "application/font-sfnt"]) }, ".pfb": { format: "PFB", mimeTypes: new Set(["application/x-font-type1", "application/x-font-pfb"]) }, } as const satisfies Record }>; function fail(code: ErrorCode, message: string): never { throw new ExternalVFontValidationError(code, message); } function classify(sourcePath: string, mimeType: string, data: ArrayBuffer): { format: ExternalVFontFormat; mimeType: string } { const extension = sourcePath.slice(sourcePath.lastIndexOf(".")).toLowerCase() as keyof typeof FORMAT; const declaration = FORMAT[extension]; if (!declaration || !declaration.mimeTypes.has(mimeType as never)) { fail("NON_MESH_BINARY_INVALID", "font extension and MIME type must agree on TTF, OTF or PFB"); } const bytes = new Uint8Array(data); const sfnt = bytes.byteLength >= 4 && bytes[0] === 0x00 && bytes[1] === 0x01 && bytes[2] === 0x00 && bytes[3] === 0x00; const otto = bytes.byteLength >= 4 && bytes[0] === 0x4f && bytes[1] === 0x54 && bytes[2] === 0x54 && bytes[3] === 0x4f; const pfb = bytes.byteLength >= 6 && bytes[0] === 0x80 && bytes[1] === 0x01; if ( (declaration.format === "TTF" && !sfnt) || (declaration.format === "OTF" && !otto) || (declaration.format === "PFB" && !pfb) ) { fail("NON_MESH_BINARY_INVALID", `font bytes do not match the declared ${declaration.format} format`); } return { format: declaration.format, mimeType }; } async function sha256(data: ArrayBuffer): Promise { return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", data))).map((byte) => byte.toString(16).padStart(2, "0")).join(""); } function mainName(fileName: string, digest: string): string { const candidate = fileName.replace(/\.[^.]+$/, ""); return candidate && new TextEncoder().encode(candidate).byteLength <= 63 ? candidate : `ExternalFont-${digest.slice(0, 12)}`; } export function validateExternalVFontMainImportProof(value: ExternalVFontMainImportProofIR): ExternalVFontMainImportProofIR { if (!value || typeof value !== "object" || value.schemaVersion !== EXTERNAL_VFONT_SCHEMA_VERSION) { fail("NON_MESH_BINARY_INVALID", "external VFont Main import proof schema is invalid"); } if (!PROJECT_ID.test(value.projectId) || !SHA256.test(value.sha256)) { fail("NON_MESH_BINARY_INVALID", "external VFont project or hash identity is invalid"); } if (value.assetId !== `sha256:${value.sha256}` || value.assetPath !== `projects/${value.projectId}/assets/sha256/${value.sha256.slice(0, 2)}/${value.sha256}`) { fail("NON_MESH_RESOURCE_MISSING", "external VFont must reference its verified OPFS content-addressed asset"); } let normalized: string; try { normalized = normalizeProjectAssetPath(value.sourcePath); } catch { fail("NON_MESH_RESOURCE_OUTSIDE_PROJECT", "external VFont Main source path is invalid"); } if (!normalized.startsWith("fonts/") || value.sourcePath !== `//${normalized}` || typeof value.name !== "string" || value.name.length === 0 || new TextEncoder().encode(value.name).byteLength > 63 || !Number.isSafeInteger(value.byteLength) || value.byteLength < 1 || value.byteLength > EXTERNAL_VFONT_MAX_BYTES) { fail("NON_MESH_BINARY_INVALID", "external VFont Main metadata is invalid"); } const declared = FORMAT[normalized.slice(normalized.lastIndexOf(".")).toLowerCase() as keyof typeof FORMAT]; if (!declared || declared.format !== value.format || !declared.mimeTypes.has(value.mimeType as never)) { fail("NON_MESH_BINARY_INVALID", "external VFont Main format and MIME type do not agree"); } return { ...value }; } export function createExternalVFontMainImport( validated: ValidatedExternalVFontIR, stored: StorageAssetPutResult, ): ExternalVFontMainImportIR { const normalized = validated.sourcePath.slice(2); if (!stored.persisted || stored.projectId.length === 0 || stored.assetId !== `sha256:${validated.sha256}` || stored.sha256 !== validated.sha256 || stored.bytes !== validated.byteLength || stored.mimeType !== validated.mimeType || stored.sourcePath !== normalized) { fail("ASSET_SOURCE_HASH_MISMATCH", "stored external VFont receipt does not match the validated font"); } const proof = validateExternalVFontMainImportProof({ schemaVersion: EXTERNAL_VFONT_SCHEMA_VERSION, projectId: stored.projectId, assetId: stored.assetId, assetPath: stored.path, sourcePath: validated.sourcePath, name: mainName(validated.fileName, validated.sha256), format: validated.format, mimeType: validated.mimeType, byteLength: validated.byteLength, sha256: validated.sha256, }); return { ...proof, data: validated.data.slice(0) }; } export async function validateStoredExternalVFontAsset( projectId: string, declaredSha256: string, stored: StorageAssetReadResult, ): Promise { if (!PROJECT_ID.test(projectId) || !SHA256.test(declaredSha256) || !stored || typeof stored !== "object" || !(stored.data instanceof ArrayBuffer)) { fail("NON_MESH_RESOURCE_MISSING", "stored external VFont asset identity is invalid"); } const asset = stored.asset; const expectedPath = `projects/${projectId}/assets/sha256/${declaredSha256.slice(0, 2)}/${declaredSha256}`; if (!asset || asset.projectId !== projectId || asset.assetId !== `sha256:${declaredSha256}` || asset.sha256 !== declaredSha256 || asset.path !== expectedPath || typeof asset.sourcePath !== "string") { fail("NON_MESH_RESOURCE_MISSING", "stored external VFont asset is not the declared project asset"); } if (asset.bytes !== stored.data.byteLength) { fail("ASSET_SOURCE_HASH_MISMATCH", "stored external VFont byte length does not match its metadata"); } return validateExternalVFontImport({ sourcePath: `//${asset.sourcePath}`, mimeType: asset.mimeType, byteLength: asset.bytes, sha256: asset.sha256, data: stored.data, }); } export async function validateExternalVFontImport(request: ExternalVFontImportRequestIR): Promise { if (!request || typeof request !== "object") fail("NON_MESH_BINARY_INVALID", "font import request is missing"); let normalized: string; try { normalized = normalizeProjectAssetPath(request.sourcePath); } catch { fail("NON_MESH_RESOURCE_OUTSIDE_PROJECT", "font source must be a project-relative path without traversal or URI syntax"); } if (!normalized.startsWith("fonts/") || new TextEncoder().encode(normalized).byteLength > 1021) { fail("NON_MESH_RESOURCE_OUTSIDE_PROJECT", "font source must be a bounded path under the project fonts directory"); } if (!(request.data instanceof ArrayBuffer)) fail("NON_MESH_BINARY_INVALID", "font payload must be an ArrayBuffer"); if (!Number.isSafeInteger(request.byteLength) || request.byteLength < 1 || request.byteLength > EXTERNAL_VFONT_MAX_BYTES) { fail("NON_MESH_DATA_BUDGET_EXCEEDED", "font payload exceeds the 32 MiB import budget"); } if (request.data.byteLength !== request.byteLength) fail("NON_MESH_BINARY_INVALID", "font declared byte length does not match its payload"); if (typeof request.mimeType !== "string" || request.mimeType.length > 128) fail("NON_MESH_BINARY_INVALID", "font MIME type is invalid"); const classified = classify(normalized, request.mimeType.toLowerCase(), request.data); if (typeof request.sha256 !== "string" || !SHA256.test(request.sha256)) fail("ASSET_SOURCE_HASH_MISMATCH", "font SHA-256 declaration is invalid"); const actualSha256 = await sha256(request.data); if (actualSha256 !== request.sha256) fail("ASSET_SOURCE_HASH_MISMATCH", "font bytes do not match the declared SHA-256"); return { schemaVersion: EXTERNAL_VFONT_SCHEMA_VERSION, sourcePath: `//${normalized}`, fileName: normalized.slice(normalized.lastIndexOf("/") + 1), format: classified.format, mimeType: classified.mimeType, byteLength: request.byteLength, sha256: actualSha256, data: request.data.slice(0), }; }