import path from 'node:path'; import { fileURLToPath } from 'node:url'; const REQUIRED = [ 'DOMAIN', 'ACME_EMAIL', 'CORE_REPOSITORY_URL', 'POSTGRES_PASSWORD', 'REDIS_PASSWORD', 'GAME_TOKEN_SECRET', 'GATEWAY_BOOTSTRAP_TOKEN', 'INITIAL_ADMIN_USERNAME', 'INITIAL_ADMIN_PASSWORD', 'KAKAO_REST_KEY', ]; const MIN_LENGTH = new Map([ ['POSTGRES_PASSWORD', 24], ['REDIS_PASSWORD', 24], ['GAME_TOKEN_SECRET', 32], ['GATEWAY_BOOTSTRAP_TOKEN', 32], ['INITIAL_ADMIN_PASSWORD', 16], ]); const isPlaceholder = (value) => value.startsWith('replace-with-') || value.includes('example.com') || value.includes('your-org'); const decodeBase64 = (value) => { try { if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 !== 0) return null; return Buffer.from(value, 'base64').toString('utf8'); } catch { return null; } }; export const validateEnvironment = (env) => { const errors = []; for (const key of REQUIRED) { const value = env[key]?.trim() ?? ''; if (!value) errors.push(`${key} is required`); else if (isPlaceholder(value)) errors.push(`${key} still contains an example placeholder`); } for (const [key, length] of MIN_LENGTH) { const value = env[key] ?? ''; if (value && value.length < length) errors.push(`${key} must contain at least ${length} characters`); } const domain = env.DOMAIN?.trim() ?? ''; if (domain && !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(domain)) { errors.push('DOMAIN must be a hostname without a scheme, path, or port'); } const caddySiteAddress = env.CADDY_SITE_ADDRESS?.trim() ?? ''; if (caddySiteAddress) { try { const parsed = new URL(caddySiteAddress); if (!['http:', 'https:'].includes(parsed.protocol) || parsed.hostname !== domain || parsed.pathname !== '/') { errors.push('CADDY_SITE_ADDRESS must be http://DOMAIN or https://DOMAIN'); } } catch { errors.push('CADDY_SITE_ADDRESS must be http://DOMAIN or https://DOMAIN'); } } const email = env.ACME_EMAIL?.trim() ?? ''; if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errors.push('ACME_EMAIL must be a valid email address'); const repositoryUrl = env.CORE_REPOSITORY_URL?.trim() ?? ''; if (repositoryUrl) { if (/^https?:\/\//i.test(repositoryUrl)) { try { const parsed = new URL(repositoryUrl); if (parsed.username || parsed.password) errors.push('CORE_REPOSITORY_URL must not embed credentials'); } catch { errors.push('CORE_REPOSITORY_URL is not a valid HTTP(S) URL'); } } else if (!/^(?:ssh:\/\/|git@)[^\s]+/i.test(repositoryUrl)) { errors.push('CORE_REPOSITORY_URL must use HTTP(S) or SSH'); } } const httpsUser = env.CORE_REPOSITORY_USERNAME?.trim() ?? ''; const httpsToken = env.CORE_REPOSITORY_TOKEN?.trim() ?? ''; const sshKey = env.CORE_SSH_PRIVATE_KEY_BASE64?.trim() ?? ''; const sshHosts = env.CORE_SSH_KNOWN_HOSTS_BASE64?.trim() ?? ''; if (Boolean(httpsUser) !== Boolean(httpsToken)) { errors.push('CORE_REPOSITORY_USERNAME and CORE_REPOSITORY_TOKEN must be set together'); } if (Boolean(sshKey) !== Boolean(sshHosts)) { errors.push('CORE_SSH_PRIVATE_KEY_BASE64 and CORE_SSH_KNOWN_HOSTS_BASE64 must be set together'); } if ((httpsUser || httpsToken) && (sshKey || sshHosts)) errors.push('configure only one Core repository authentication mode'); if ((httpsUser || httpsToken) && !/^https?:\/\//i.test(repositoryUrl)) { errors.push('HTTPS repository credentials require an HTTP(S) CORE_REPOSITORY_URL'); } if ((sshKey || sshHosts) && !/^(?:ssh:\/\/|git@)/i.test(repositoryUrl)) { errors.push('SSH repository credentials require an SSH CORE_REPOSITORY_URL'); } if (sshKey) { const decoded = decodeBase64(sshKey); if (!decoded?.includes('BEGIN OPENSSH PRIVATE KEY')) { errors.push('CORE_SSH_PRIVATE_KEY_BASE64 must encode an OpenSSH private key'); } } if (sshHosts && !decodeBase64(sshHosts)?.trim()) { errors.push('CORE_SSH_KNOWN_HOSTS_BASE64 must contain valid base64-encoded known_hosts data'); } return errors; }; const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]); if (isMain) { const errors = validateEnvironment(process.env); if (errors.length) { console.error('Environment validation failed:'); for (const error of errors) console.error(`- ${error}`); process.exitCode = 64; } else { console.log('Environment validation passed without printing secret values.'); } }