import { spawn } from 'node:child_process'; const env = { ...process.env, API_HOST: '127.0.0.1', API_PORT: '3001' }; function origin(name) { const value = env[name]; if (!value) return undefined; const url = new URL(value); if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/') { throw new Error(`${name} must be an HTTP(S) origin without a path`); } if (env.COOKIE_SECURE !== 'false' && url.protocol !== 'https:') throw new Error(`${name} requires HTTPS with secure cookies`); return url.origin; } const children = new Set(); let stopping = false; let exitCode = 0; let killTimer; function stop(code) { if (stopping) return; stopping = true; exitCode = code; process.exitCode = code; for (const child of children) child.kill('SIGTERM'); if (children.size) { killTimer = setTimeout(() => { for (const child of children) child.kill('SIGKILL'); }, 10000); killTimer.unref(); } } process.on('SIGTERM', () => stop(0)); process.on('SIGINT', () => stop(0)); function run(command, args, service = false) { return new Promise((resolve, reject) => { if (stopping) { resolve(); return; } const child = spawn(command, args, { env, stdio: 'inherit' }); children.add(child); child.once('error', () => { stop(1); reject(new Error(`Could not start ${command}`)); }); child.once('close', code => { children.delete(child); if (service && !stopping) { console.error(`${command} exited unexpectedly; stopping the container`); stop(1); } if (!children.size && killTimer) clearTimeout(killTimer); if (!stopping && code !== 0) reject(new Error(`${command} failed with exit code ${code}`)); else resolve(); }); }); } try { const appUrl = origin('APP_URL'); const adminUrl = origin('ADMIN_URL'); if (!env.APP_ORIGINS && !appUrl) throw new Error('APP_URL (or APP_ORIGINS) is required'); env.APP_ORIGINS ||= [...new Set([appUrl, adminUrl].filter(Boolean))].join(','); if (appUrl) env.OIDC_REDIRECT_URI ||= `${appUrl}/api/auth/callback`; if (!env.DB_HOST || !env.DB_PASSWORD) throw new Error('DB_HOST and DB_PASSWORD are required'); if (!env.TOKEN_ENCRYPTION_KEY || Buffer.from(env.TOKEN_ENCRYPTION_KEY, 'base64').length !== 32) throw new Error('TOKEN_ENCRYPTION_KEY must contain 32 random bytes encoded as base64'); await run('node', ['dist/migrate.js']); await run('node', ['dist/seed.js']); if (!stopping) await Promise.all([ run('node', ['dist/server.js'], true), run('nginx', ['-g', 'daemon off;'], true), ]); } catch (error) { // Configuration values, passwords, and tokens are never printed here. console.error(error instanceof Error ? error.message : 'Container startup failed'); stop(1); } process.exitCode = exitCode;