import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { dirname, resolve } from 'node:path'; import { brandAssets } from '../src/lib/brand-assets'; // User-provided design. Credentials are read only from the local environment. const fileKey = '1vUq4wf5xOs8ZOlmt26Q3O'; const token = process.env.FIGMA_TOKEN; const assets = [ { id: '1:414', name: 'header', file: 'public/static/brand/header.svg', width: 104, height: 36 }, { id: '1:706', name: 'footer', file: 'public/static/brand/footer.svg', width: 124, height: 44 }, { id: '3:20211', name: 'mobile', file: 'public/static/brand/mobile.svg', width: 88, height: 28 }, { id: '3:15722', name: 'symbol', file: 'public/static/favicon.svg', width: 34, height: 34 }, ] as const; const manifestFile = resolve('public/static/brand/manifest.json'); const pngAssets = [ { id: '1:414', name: 'header-png', file: 'public/static/logo.png', width: 208, height: 72 }, { id: '1:706', name: 'footer-png', file: 'public/static/logo2.png', width: 248, height: 88 }, { id: '3:21847', name: 'login-symbol', file: 'public/static/logo_icon.png', width: 128, height: 128 }, { id: '3:15722', name: 'favicon-png', file: 'public/static/favicon.png', width: 68, height: 68 }, { id: '3:15722', name: 'favicon-ico', file: 'public/static/favicon.ico', width: 68, height: 68 }, ]; const alias = (asset: typeof pngAssets[number], url: string) => ({ ...asset, name: `${asset.name}-versioned`, source: asset.file, file: `public${url}`, }); const versionedAssets = [ alias(pngAssets[0]!, brandAssets.headerPng), alias(pngAssets[1]!, brandAssets.navigationLogo), alias(pngAssets[2]!, brandAssets.authLogo), alias(assets[3], brandAssets.iconSvg), alias(pngAssets[3]!, brandAssets.iconPng), alias(pngAssets[4]!, brandAssets.faviconIco), ]; const sha256 = (bytes: Buffer): string => createHash('sha256').update(bytes).digest('hex'); if (process.argv.includes('--check')) { const manifest = JSON.parse(await readFile(manifestFile, 'utf8')) as { assets: { file: string; sha256: string; width: number; height: number }[] }; for (const asset of manifest.assets) { const bytes = await readFile(resolve(asset.file)); if (sha256(bytes) !== asset.sha256) throw new Error(`Brand asset checksum mismatch: ${asset.file}`); if (asset.file.endsWith('.png') && (bytes.readUInt32BE(16) !== asset.width || bytes.readUInt32BE(20) !== asset.height)) throw new Error(`PNG dimensions mismatch: ${asset.file}`); } for (const asset of versionedAssets) { if (!manifest.assets.some(entry => entry.file === asset.file)) throw new Error(`Versioned brand asset missing from manifest: ${asset.file}`); if (!(await readFile(resolve(asset.file))).equals(await readFile(resolve(asset.source)))) throw new Error(`Versioned brand asset differs from source: ${asset.file}`); } const ico = await readFile(resolve('public/static/favicon.ico')); const png = await readFile(resolve('public/static/favicon.png')); if (!ico.subarray(22).equals(png)) throw new Error('ICO payload differs from exported PNG'); console.log(`Checked ${manifest.assets.length} Figma brand assets offline.`); process.exit(0); } if (!token) throw new Error('FIGMA_TOKEN is required'); async function render(ids: readonly string[], format: 'svg' | 'png', scale = 1): Promise> { const query = new URLSearchParams({ ids: ids.join(','), format, scale: String(scale), svg_outline_text: 'true' }); const response = await fetch(`https://api.figma.com/v1/images/${fileKey}?${query}`, { headers: { 'X-Figma-Token': token! } }); if (!response.ok) throw new Error(`Figma render returned HTTP ${response.status}`); const result = await response.json() as { images: Record }; for (const id of ids) if (!result.images[id]) throw new Error(`Figma returned no image for ${id}`); return result.images as Record; } async function download(url: string, file: string): Promise { const response = await fetch(url); if (!response.ok) throw new Error(`Asset download returned HTTP ${response.status}`); const bytes = Buffer.from(await response.arrayBuffer()); if (!bytes.length) throw new Error('Empty Figma asset'); await mkdir(dirname(resolve(file)), { recursive: true }); await writeFile(resolve(file), bytes); return bytes; } const svg = await render(assets.map(asset => asset.id), 'svg'); for (const asset of assets) await download(svg[asset.id], asset.file); const png = await render(['1:414', '1:706', '3:21847', '3:15722'], 'png', 2); await download(png['1:414'], 'public/static/logo.png'); await download(png['1:706'], 'public/static/logo2.png'); await download(png['3:21847'], 'public/static/logo_icon.png'); const icon = await download(png['3:15722'], 'public/static/favicon.png'); // ICO supports an unmodified PNG payload; no redraw, recoloring or raster resampling. const icoHeader = Buffer.alloc(22); icoHeader.writeUInt16LE(1, 2); icoHeader.writeUInt16LE(1, 4); icoHeader[6] = 68; icoHeader[7] = 68; icoHeader.writeUInt16LE(1, 10); icoHeader.writeUInt16LE(32, 12); icoHeader.writeUInt32LE(icon.length, 14); icoHeader.writeUInt32LE(22, 18); await writeFile(resolve('public/static/favicon.ico'), Buffer.concat([icoHeader, icon])); for (const asset of versionedAssets) { await mkdir(dirname(resolve(asset.file)), { recursive: true }); await writeFile(resolve(asset.file), await readFile(resolve(asset.source))); } const manifestAssets = await Promise.all([...assets, ...pngAssets, ...versionedAssets].map(async asset => ({ ...asset, sha256: sha256(await readFile(resolve(asset.file))) }))); await writeFile(manifestFile, JSON.stringify({ fileKey, source: `https://www.figma.com/design/${fileKey}/?node-id=0-1`, assets: manifestAssets }, null, 2) + '\n'); console.log(`Downloaded ${assets.length} SVG brand assets and matching PNG/ICO fallbacks from Figma; generated ${versionedAssets.length} versioned aliases.`);