File size: 4,920 Bytes
c9996a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
 * 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);
}