Spaces:
Paused
Paused
| 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); | |
| } | |