125 lines
4.5 KiB
JavaScript
125 lines
4.5 KiB
JavaScript
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.');
|
|
}
|
|
}
|