mirror of
https://github.com/katelya77/K-Vault.git
synced 2026-05-06 22:10:57 +08:00
182 lines
4.5 KiB
JavaScript
182 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const repoRoot = path.resolve(__dirname, '..');
|
|
const keyTargets = ['CONFIG_ENCRYPTION_KEY', 'SESSION_SECRET'];
|
|
|
|
const placeholderValues = new Set([
|
|
'replace_with_a_long_random_secret',
|
|
'replace_with_another_long_random_secret',
|
|
'replace_me',
|
|
'change_me',
|
|
'changeme',
|
|
'your_secret_here',
|
|
'your_session_secret',
|
|
'placeholder',
|
|
]);
|
|
|
|
const placeholderPatterns = [
|
|
/^replace_with/i,
|
|
/^change_this/i,
|
|
/^your[_-]?(secret|password)/i,
|
|
/^example$/i,
|
|
/^default$/i,
|
|
/^todo$/i,
|
|
/^<.+>$/,
|
|
];
|
|
|
|
function usage() {
|
|
console.log('Usage: node scripts/bootstrap-env.js [--env-path path] [--example-path path]');
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const output = {
|
|
envFile: path.join(repoRoot, '.env'),
|
|
exampleFile: path.join(repoRoot, '.env.example'),
|
|
};
|
|
|
|
for (let i = 2; i < argv.length; i += 1) {
|
|
const arg = argv[i];
|
|
|
|
if (arg === '--help' || arg === '-h') {
|
|
output.help = true;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--env-path' && argv[i + 1]) {
|
|
output.envFile = path.resolve(repoRoot, argv[i + 1]);
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (arg.startsWith('--env-path=')) {
|
|
output.envFile = path.resolve(repoRoot, arg.slice('--env-path='.length));
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--example-path' && argv[i + 1]) {
|
|
output.exampleFile = path.resolve(repoRoot, argv[i + 1]);
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (arg.startsWith('--example-path=')) {
|
|
output.exampleFile = path.resolve(repoRoot, arg.slice('--example-path='.length));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
function escapeRegExp(input) {
|
|
return String(input).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
function getEol(text) {
|
|
return text.includes('\r\n') ? '\r\n' : '\n';
|
|
}
|
|
|
|
function parseEnvValue(raw) {
|
|
let value = String(raw || '').trim();
|
|
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"'))
|
|
|| (value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
return value.slice(1, -1);
|
|
}
|
|
|
|
const commentPos = value.indexOf(' #');
|
|
if (commentPos >= 0) {
|
|
value = value.slice(0, commentPos).trim();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function readEnvValue(content, key) {
|
|
const matcher = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=\\s*(.*)$`, 'm');
|
|
const matched = content.match(matcher);
|
|
if (!matched) return '';
|
|
return parseEnvValue(matched[1]);
|
|
}
|
|
|
|
function upsertEnvValue(content, key, value) {
|
|
const eol = getEol(content || '\n');
|
|
const matcher = new RegExp(`^(\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=\\s*).*$`, 'm');
|
|
|
|
if (matcher.test(content)) {
|
|
return content.replace(matcher, (_, prefix) => `${prefix}${value}`);
|
|
}
|
|
|
|
const suffix = content && !content.endsWith('\n') && !content.endsWith('\r\n') ? eol : '';
|
|
return `${content}${suffix}${key}=${value}${eol}`;
|
|
}
|
|
|
|
function isPlaceholder(value) {
|
|
const normalized = String(value || '').trim();
|
|
if (!normalized) return true;
|
|
if (placeholderValues.has(normalized)) return true;
|
|
return placeholderPatterns.some((pattern) => pattern.test(normalized));
|
|
}
|
|
|
|
function generateSecret() {
|
|
return crypto.randomBytes(48).toString('base64url');
|
|
}
|
|
|
|
function ensureEnvFile(envFile, exampleFile) {
|
|
if (fs.existsSync(envFile)) {
|
|
return false;
|
|
}
|
|
if (!fs.existsSync(exampleFile)) {
|
|
throw new Error(`Template file not found: ${exampleFile}`);
|
|
}
|
|
|
|
fs.copyFileSync(exampleFile, envFile);
|
|
return true;
|
|
}
|
|
|
|
function run() {
|
|
const options = parseArgs(process.argv);
|
|
if (options.help) {
|
|
usage();
|
|
return;
|
|
}
|
|
|
|
const envFile = options.envFile;
|
|
const exampleFile = options.exampleFile;
|
|
|
|
const created = ensureEnvFile(envFile, exampleFile);
|
|
let content = fs.readFileSync(envFile, 'utf8');
|
|
const generatedKeys = [];
|
|
|
|
for (const key of keyTargets) {
|
|
const current = readEnvValue(content, key);
|
|
if (!isPlaceholder(current)) continue;
|
|
content = upsertEnvValue(content, key, generateSecret());
|
|
generatedKeys.push(key);
|
|
}
|
|
|
|
if (generatedKeys.length > 0) {
|
|
fs.writeFileSync(envFile, content, 'utf8');
|
|
}
|
|
|
|
if (created) {
|
|
console.log(`[bootstrap-env] Created ${path.basename(envFile)} from ${path.basename(exampleFile)}.`);
|
|
}
|
|
if (generatedKeys.length > 0) {
|
|
console.log(`[bootstrap-env] Generated secure values for: ${generatedKeys.join(', ')}.`);
|
|
} else {
|
|
console.log('[bootstrap-env] CONFIG_ENCRYPTION_KEY and SESSION_SECRET already configured, no changes made.');
|
|
}
|
|
}
|
|
|
|
try {
|
|
run();
|
|
} catch (error) {
|
|
console.error(`[bootstrap-env] ${error.message}`);
|
|
process.exit(1);
|
|
}
|