/** * Build verification for Flux Studio — Zero GPU. * * Runs as `npm run build` inside the Docker image (and locally before * every push). It fails loudly if anything in the app is broken, so a * broken build can never reach the Hugging Face builder silently. * * Checks: * 1. All required files exist. * 2. server.mjs parses and imports cleanly (no listen — import only). * 3. Config defaults match the spec * (flux-2-pro-preview primary, flux-2-klein-9b fallback, * https://api.bfl.ai base, port 7860). * 4. Placeholder-key detection behaves. * 5. Request validation behaves (accepts good input, rejects bad). * 6. The image proxy host allowlist blocks non-BFL hosts. * 7. The static client ships index.html/app.js/style.css and the * client never references the API key. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); const failures = []; function check(name, fn) { try { fn(); console.log(` ok ${name}`); } catch (err) { failures.push({ name, message: err.message }); console.error(` FAIL ${name}: ${err.message}`); } } function assert(cond, msg) { if (!cond) throw new Error(msg); } /* 1. Required files --------------------------------------------------- */ const REQUIRED = [ 'server.mjs', 'package.json', 'package-lock.json', 'Dockerfile', 'README.md', 'public/index.html', 'public/style.css', 'public/app.js', ]; check('required files exist', () => { for (const f of REQUIRED) { assert(fs.existsSync(path.join(root, f)), `missing file: ${f}`); } }); /* 2/3. Server module imports cleanly with spec defaults --------------- */ const server = await import(path.join(root, 'server.mjs')); check('config defaults match the spec', () => { const { config } = server; assert(config.modelId === 'flux-2-pro-preview', `modelId=${config.modelId}, expected flux-2-pro-preview`); assert(config.fallbackModelId === 'flux-2-klein-9b', `fallbackModelId=${config.fallbackModelId}, expected flux-2-klein-9b`); assert(config.apiBaseHost === 'api.bfl.ai', `apiBaseHost=${config.apiBaseHost}, expected api.bfl.ai`); assert(config.port === 7860, `port=${config.port}, expected 7860`); assert(typeof config.ready === 'boolean', 'config.ready should be boolean'); }); /* 4. Placeholder detection -------------------------------------------- */ check('placeholder key detection', () => { assert(server.isPlaceholderKey('') === true, 'empty key must count as placeholder'); assert(server.isPlaceholderKey('placeholder-replace-with-real-bfl-key') === true, 'placeholder value must count'); assert(server.isPlaceholderKey('sk-1234567890abcdef') === false, 'normal-looking key must not count'); }); /* 5. Validation -------------------------------------------------------- */ check('request validation', () => { const good = server.validateBody({ prompt: 'a cat', aspectRatio: '16:9', outputFormat: 'jpeg' }); assert(good.width === 1344 && good.height === 768, `aspect preset wrong: ${good.width}x${good.height}`); const seeded = server.validateBody({ prompt: 'a cat', width: 1024, height: 1024, seed: '42' }); assert(seeded.seed === 42, 'seed coercion failed'); let threw = false; try { server.validateBody({ prompt: '', width: 99999 }); } catch { threw = true; } assert(threw, 'invalid body must throw'); const payload = server.buildPayload({ prompt: 'x', width: 256, height: 256, outputFormat: 'png', seed: 7 }); assert(payload.seed === 7 && payload.output_format === 'png', 'BFL payload mapping wrong'); }); /* 6. Image proxy allowlist --------------------------------------------- */ check('image proxy host allowlist', () => { assert(server.isAllowedDeliveryHost('delivery.bfl.ai') === true, 'delivery.bfl.ai should be allowed'); assert(server.isAllowedDeliveryHost('bflai.blob.core.windows.net') === true, 'azure blob host should be allowed'); assert(server.isAllowedDeliveryHost('evil.example.com') === false, 'random host must be blocked'); assert(server.isAllowedDeliveryHost('bfl.ai.evil.com') === false, 'suffix spoof must be blocked'); }); /* 7. Client hygiene ---------------------------------------------------- */ check('client has no server secret access', () => { const appJs = fs.readFileSync(path.join(root, 'public/app.js'), 'utf8'); assert(!/BFL_API_KEY\s*=/.test(appJs), 'client must not assign BFL_API_KEY'); assert(!/x-key/i.test(appJs), 'client must not set the x-key header'); }); /* ---------------------------------------------------------------------- */ console.log(''); if (failures.length) { console.error(`BUILD FAILED — ${failures.length} check(s) failed.`); process.exit(1); } else { console.log('BUILD OK — all checks passed.'); process.exit(0); }