bws-secure
Version:
Secure environment management with Bitwarden Secrets Manager
1,014 lines (885 loc) • 39.3 kB
JavaScript
import { execSync, spawnSync } from 'node:child_process';
import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import * as fsPromises from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import dotenv from 'dotenv';
import { ensureBwsInstalled } from './bws-dotenv.js';
import {
promptForProject,
updateEnvironmentBwsSection,
determineEnvironment,
normalizeEnvironment,
readConfigFile,
log
} from './project-selector.js';
import { validateDeployment } from './update-environments/utils.js';
// Import functions from project-selector module
// Get the directory name in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Add flag to detect nested execution
const isNestedExecution = process.env.BWS_SECURE_RUN_ACTIVE === 'true';
// Only set env var if this is the first execution
if (!isNestedExecution) {
process.env.BWS_SECURE_RUN_ACTIVE = 'true';
} else {
log('info', 'Detected nested secure-run execution - using parent environment');
}
// Use current working directory for project root
const projectRoot = process.cwd();
// 1) Try loading .env from the repo root first
const dotenvPath = path.join(projectRoot, '.env');
dotenv.config({ path: dotenvPath });
// Platform detection
const isNetlify = process.env.NETLIFY === 'true';
const isVercel = process.env.VERCEL === '1';
const isDebug = process.env.DEBUG === 'true';
const isWindows = process.platform === 'win32';
// Helper function for debug logging
function debugLog(message) {
if (isDebug) {
console.log(message);
}
}
// File holding original content
let originalEnvironmentFileContent = null;
// This function will be called by the main() function
// to ensure that the requiredVars.env file exists
async function ensureRequiredVariablesFile() {
debugLog('Ensuring required variables file exists...');
// Scan for required variables
const scanResult = spawnSync(
'node',
[path.join(__dirname, 'check-vars', 'requiredRuntimeVars.js')],
{
stdio: 'inherit',
env: process.env
}
);
if (scanResult.status !== 0) {
throw new Error('Failed to scan for required environment variables');
}
const requiredVariablesPath = path.join(process.cwd(), 'requiredVars.env');
if (!fs.existsSync(requiredVariablesPath)) {
throw new Error('Required variables file was not generated');
}
return requiredVariablesPath;
}
// After loading dotenv but before setupEnvironment
async function createRequiredVariablesFile() {
try {
const environmentPath = path.join(projectRoot, '.env');
if (!fs.existsSync(environmentPath)) {
log('warn', 'No .env file found to create requiredVars.env');
return;
}
log('info', 'Creating requiredVars.env from .env file...');
// Extract variable names from the .env file
const envFileContent = await fsPromises.readFile(environmentPath, 'utf8');
const lines = envFileContent.split('\n');
const variables = lines
.filter((line) => line.trim() && !line.trim().startsWith('#') && line.includes('='))
.map((line) => line.split('=')[0].trim()); // Extract just the variable names
// Create the requiredVars.env file in the correct location
const requiredVariablesPath = path.join(process.cwd(), 'requiredVars.env');
const requiredVariablesContent = variables.join('\n');
fs.writeFileSync(requiredVariablesPath, requiredVariablesContent);
log('info', `Created requiredVars.env with ${variables.length} variables`);
} catch (error) {
log('error', `Error creating requiredVars.env: ${error.message}`);
}
}
// Helper function to get BWS command with proper path resolution
function getBwsCommand() {
// First check in project node_modules
const projectBinDir = path.join(projectRoot, 'node_modules', '.bin');
const projectExePath = isWindows
? path.join(projectBinDir, 'bws.exe')
: path.join(projectBinDir, 'bws');
if (fs.existsSync(projectExePath)) {
return projectExePath;
}
// If not found, check for global installation
try {
const whichCommand = isWindows ? 'where bws' : 'which bws';
const globalPath = execSync(whichCommand, { encoding: 'utf8' }).trim();
if (globalPath && fs.existsSync(globalPath)) {
return globalPath;
}
} catch (e) {
// Ignore errors from which/where command
}
// Fallback to just the command name (rely on PATH)
return isWindows ? 'bws.exe' : 'bws';
}
// Add helper to validate BWS token
async function validateBwsToken() {
if (!process.env.BWS_ACCESS_TOKEN) {
// prettier-ignore
{
console.warn('\u001B[33m╔════════════════════════════════════════════════════════╗\u001B[0m');
console.warn('\u001B[33m║ ║\u001B[0m');
console.warn('\u001B[33m║ WARNING: BWS TOKEN MISSING ║\u001B[0m');
console.warn('\u001B[33m║ ║\u001B[0m');
console.warn('\u001B[33m║ To use BWS features: ║\u001B[0m');
console.warn('\u001B[33m║ 1. Log in to vault.bitwarden.com ║\u001B[0m');
console.warn('\u001B[33m║ 2. Go to Secrets Manager > Machine Accounts ║\u001B[0m');
console.warn('\u001B[33m║ 3. Create or copy your machine access token ║\u001B[0m');
console.warn('\u001B[33m║ 4. Add to .env: BWS_ACCESS_TOKEN=your_token ║\u001B[0m');
console.warn('\u001B[33m║ ║\u001B[0m');
console.warn('\u001B[33m║ For now, continuing with only .env values... ║\u001B[0m');
console.warn('\u001B[33m║ ║\u001B[0m');
console.warn('\u001B[33m╚════════════════════════════════════════════════════════╝\u001B[0m');
console.warn(
'\nVisit the link below to create your token: \n' +
'\nhttps://vault.bitwarden.com/#/sm/22479128-f194-460a-884b-b24a015686c6/machine-accounts\n'
);
}
return false;
}
try {
// 2) Replace "./node_modules/.bin/bws" with the helper function
execSync(`${getBwsCommand()} project list -t ${process.env.BWS_ACCESS_TOKEN}`, {
stdio: 'ignore',
env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' }
});
return true;
} catch {
// prettier-ignore
{
console.error('\u001B[31m╔════════════════════════════════════════════════════════╗\u001B[0m');
console.error('\u001B[31m║ ║\u001B[0m');
console.error('\u001B[31m║ CRITICAL BWS TOKEN ERROR ║\u001B[0m');
console.error('\u001B[31m║ ║\u001B[0m');
console.error('\u001B[31m║ Your BWS_ACCESS_TOKEN appears to be invalid: ║\u001B[0m');
console.error('\u001B[31m║ 1. Check if token has expired ║\u001B[0m');
console.error('\u001B[31m║ 2. Verify token permissions in vault.bitwarden.com ║\u001B[0m');
console.error('\u001B[31m║ 3. Generate new token if needed ║\u001B[0m');
console.error('\u001B[31m║ 4. Ensure token has read access to required projects ║\u001B[0m');
console.error('\u001B[31m║ ║\u001B[0m');
console.error('\u001B[31m║ For now, continuing with only .env values... ║\u001B[0m');
console.error('\u001B[31m║ ║\u001B[0m');
console.error('\u001B[31m╚════════════════════════════════════════════════════════╝\u001B[0m');
console.error(
'\nVisit the link below to check or regenerate your token: \n' +
'\nhttps://vault.bitwarden.com/#/sm/22479128-f194-460a-884b-b24a015686c6/machine-accounts\n'
);
}
return false;
}
}
// Add this helper function for environment status display
function printEnvironmentSummary() {
const environment = process.env.BWS_ENV || 'local';
const project = process.env.BWS_PROJECT || 'none';
const testVariable = process.env.BWS_SECRET_TEST_VAR || 'not set';
const projectId = process.env.BWS_PROJECT_ID || 'none';
// Increased BOX_WIDTH by 10 characters (from 80 to 90)
const BOX_WIDTH = 90;
const CONTENT_START = 25; // Position where values start
// Helper to pad content
const padContent = (label, value) => {
const padding = ' '.repeat(CONTENT_START - label.length);
const available = BOX_WIDTH - CONTENT_START - 4; // -4 for borders and spacing
const truncated = value.slice(0, Math.max(0, available));
return `║ ${label}${padding}: ${truncated}${' '.repeat(available - truncated.length)} ║`;
};
const lines = [
`╔${'═'.repeat(BOX_WIDTH - 0)}╗`,
`║${' Environment Summary'.padEnd(BOX_WIDTH - 1)} ║`,
`╟${'─'.repeat(BOX_WIDTH - 0)}╢`,
padContent('Project', project),
`║${' '.repeat(BOX_WIDTH - 0)}║`,
padContent('BWS_ENV', environment),
`║${' '.repeat(BOX_WIDTH - 0)}║`,
padContent('BWS Project ID', projectId),
`║${' '.repeat(BOX_WIDTH - 0)}║`,
padContent('BWS_SECRET_TEST_VAR', testVariable),
`╚${'═'.repeat(BOX_WIDTH - 0)}╝`
];
// Add a blank line before and after the box
log('info', '');
for (const line of lines) {
log('info', line);
}
log('info', '');
}
// At the start of execution, store ALL original environment variables
const originalEnvironment = { ...process.env };
// Keep original setupEnvironment but enhance with platform support
async function setupEnvironment(options = { isPlatformBuild: false }) {
debugLog('Creating project-specific secure files...');
// Debug info only when DEBUG=true
if (isDebug) {
debugLog('Debug Info:');
debugLog(`Current working directory: ${process.cwd()}`);
debugLog(`Script directory: ${__dirname}`);
debugLog(`Node modules directory: ${path.join(process.cwd(), 'node_modules')}`);
debugLog('Searching in paths: [');
debugLog(` '${path.join(process.cwd(), 'node_modules/bws-secure/secureRun.js')}',`);
debugLog(` '../secureRun.js',`);
debugLog(` '${path.join(path.dirname(process.cwd()), 'secureRun.js')}'`);
debugLog(']');
}
// Save original .env file content if it exists
const environmentPath = path.join(projectRoot, '.env');
if (fs.existsSync(environmentPath)) {
originalEnvironmentFileContent = await fsPromises.readFile(environmentPath, 'utf8');
}
if (options.isPlatformBuild) {
debugLog('Running platform-specific setup...');
}
// Initialize tracking of created files
process.env.BWS_CREATED_FILES = '[]';
// Track which project IDs have already been loaded to avoid redundant API calls
const loadedProjectIds = new Set();
// Generate ephemeral encryption key if not exists
if (!process.env.BWS_EPHEMERAL_KEY) {
process.env.BWS_EPHEMERAL_KEY = crypto.randomBytes(32).toString('hex');
}
try {
const config = await readConfigFile();
if (!config || !config.projects) {
throw new Error('Invalid configuration file');
}
// Determine environment early
const environment = determineEnvironment();
process.env.BWS_ENV = environment;
log('debug', `Using environment: ${environment}`);
// Check for SITE_NAME early and filter projects if it exists (BEFORE downloading any secrets)
let projectsToUse = [...config.projects];
if (process.env.SITE_NAME) {
log('info', `Detected SITE_NAME=${process.env.SITE_NAME}`);
// Find the project matching SITE_NAME
const siteNameMatch = config.projects.find(
(project) => project.projectName === process.env.SITE_NAME
);
if (siteNameMatch) {
log('info', `Found matching project for SITE_NAME: ${process.env.SITE_NAME}`);
// Only use this project
projectsToUse = [siteNameMatch];
// Force BWS_PROJECT to match SITE_NAME for all operations
process.env.BWS_PROJECT = process.env.SITE_NAME;
log('info', `Forced BWS_PROJECT=${process.env.SITE_NAME} to match SITE_NAME`);
} else {
log('warn', `No project found matching SITE_NAME: ${process.env.SITE_NAME}`);
}
} else if (process.env.BWS_PROJECT) {
// When SITE_NAME is not available but BWS_PROJECT is set, use it to filter projects
log('info', `No SITE_NAME found, using BWS_PROJECT=${process.env.BWS_PROJECT}`);
const projectMatch = config.projects.find(
(project) => project.projectName === process.env.BWS_PROJECT
);
if (projectMatch) {
log('info', `Found matching project for BWS_PROJECT: ${process.env.BWS_PROJECT}`);
// Only use this project for the rest of the process
projectsToUse = [projectMatch];
} else {
log('warn', `No project found matching BWS_PROJECT: ${process.env.BWS_PROJECT}`);
}
}
// Call promptForProject which will:
// 1. Check process.env.BWS_PROJECT first
// 2. If not set, check for project in .env file
// 3. If still not found, prompt user to select a project (if multiple)
// 4. Update .env file with selected project
const selectedProject = await promptForProject(projectsToUse);
process.env.BWS_PROJECT = selectedProject.projectName;
// When neither SITE_NAME nor BWS_PROJECT was initially set, we need to filter projects now
// that we have a selection from promptForProject
if (projectsToUse.length > 1) {
log(
'info',
`Multiple projects were available, filtering to only use selected project: ${process.env.BWS_PROJECT}`
);
projectsToUse = [selectedProject];
}
// Ensure we know which project IDs we need to load for the selected project
const projectIdsToLoad = new Set();
const selectedProjectConfig = config.projects.find(
(p) => p.projectName === process.env.BWS_PROJECT
);
if (selectedProjectConfig && selectedProjectConfig.bwsProjectIds) {
Object.values(selectedProjectConfig.bwsProjectIds).forEach((id) => {
if (id) projectIdsToLoad.add(id);
});
log('debug', `Will load secrets for project IDs: ${[...projectIdsToLoad].join(', ')}`);
}
// Get unique platforms from config
const platforms = new Set(projectsToUse.map((p) => p.platform.toLowerCase()));
// Create global .env.secure with platform tokens
if (process.env.BWS_ACCESS_TOKEN) {
try {
const result = spawnSync(
getBwsCommand(),
['secret', 'list', '-t', process.env.BWS_ACCESS_TOKEN],
{
encoding: 'utf8',
env: {
...process.env,
NO_COLOR: '1',
FORCE_COLOR: '0',
TERM: 'dumb' // Add this to further discourage color output
}
}
);
if (result.status === 0) {
// Always clean the output, even without DEBUG
const cleanOutput = result.stdout.replaceAll(/\u001B\[\d+m/g, '').trim();
try {
const secrets = JSON.parse(cleanOutput);
const platformTokens = {};
const platformsFound = [];
// Only load tokens for the platform we're on
if (isNetlify && platforms.has('netlify')) {
const netlifyToken = secrets.find((s) => s.key === 'NETLIFY_AUTH_TOKEN');
if (netlifyToken) {
platformTokens.NETLIFY_AUTH_TOKEN = netlifyToken.value;
platformsFound.push('Netlify');
}
}
if (isVercel && platforms.has('vercel')) {
const vercelToken = secrets.find((s) => s.key === 'VERCEL_AUTH_TOKEN');
if (vercelToken) {
platformTokens.VERCEL_AUTH_TOKEN = vercelToken.value;
platformsFound.push('Vercel');
}
}
// Only create .env.secure if we found tokens for our platform
if (Object.keys(platformTokens).length > 0) {
const content = Object.entries(platformTokens)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
const cipherText = encryptContent(content, process.env.BWS_EPHEMERAL_KEY);
fs.writeFileSync('.env.secure', cipherText);
log('info', `✓ Successfully loaded auth tokens for: ${platformsFound.join(', ')}`);
}
} catch (error) {
log('warn', `Failed to parse secrets: ${error.message}`);
}
}
} catch (error) {
log('warn', `Failed to create global .env.secure: ${error.message}`);
}
}
// For platform builds, process each project
if (isNetlify || isVercel) {
// Save original environment values if they exist
const originalEnvironment_ = {
BWS_ENV: process.env.BWS_ENV,
BWS_PROJECT: process.env.BWS_PROJECT,
BWS_PROJECT_ID: process.env.BWS_PROJECT_ID,
BWS_ACCESS_TOKEN: process.env.BWS_ACCESS_TOKEN
};
// Re-use filtered projects from above - no need to filter again
let projectsToProcess = projectsToUse;
for (const project of projectsToProcess) {
log('info', `\n=== Processing ${project.projectName} ===`);
// Create the standard environment files for this project
const environmentMappings = {
prod: project.bwsProjectIds.prod,
dev: project.bwsProjectIds.dev,
local: project.bwsProjectIds.local
};
// Only load and create environment files that don't already exist
for (const [environment_, projectId] of Object.entries(environmentMappings)) {
// Skip null/undefined project IDs
if (!projectId) continue;
// Only load this project ID if we haven't already
if (!loadedProjectIds.has(projectId)) {
await loadEnvironmentSecrets(projectId, projectId);
loadedProjectIds.add(projectId);
}
// Create symlink or copy the file as needed
const sourceFile = `.env.secure.${projectId}`;
const targetFile = `.env.secure.${environment_}`;
if (fs.existsSync(sourceFile)) {
fs.copyFileSync(sourceFile, targetFile);
log('debug', `Created ${targetFile} from ${sourceFile}`);
}
}
// Set current project in environment
process.env.BWS_PROJECT = project.projectName;
// Enhanced platform operations
log('info', `Syncing platform variables for ${project.projectName}...`);
const updateResult = spawnSync(
'node',
[path.join(__dirname, 'update-environments', 'updateEnvVars.js')],
{
stdio: 'inherit',
env: process.env
}
);
// Clean up the standard environment files after processing this project
if (!process.env.DEBUG) {
for (const environment_ of ['prod', 'dev', 'local']) {
const file = `.env.secure.${environment_}`;
if (fs.existsSync(file)) {
fs.unlinkSync(file);
log('debug', `Cleaned up ${file}`);
}
}
}
}
// After all platform operations, restore original values if they existed
if (
originalEnvironment_.BWS_ENV ||
originalEnvironment_.BWS_PROJECT ||
originalEnvironment_.BWS_PROJECT_ID ||
originalEnvironment_.BWS_ACCESS_TOKEN
) {
if (originalEnvironment_.BWS_ENV) {
process.env.BWS_ENV = originalEnvironment_.BWS_ENV;
}
if (originalEnvironment_.BWS_PROJECT) {
process.env.BWS_PROJECT = originalEnvironment_.BWS_PROJECT;
}
if (originalEnvironment_.BWS_PROJECT_ID) {
process.env.BWS_PROJECT_ID = originalEnvironment_.BWS_PROJECT_ID;
}
if (originalEnvironment_.BWS_ACCESS_TOKEN) {
process.env.BWS_ACCESS_TOKEN = originalEnvironment_.BWS_ACCESS_TOKEN;
}
// Load the environment based on restored values
if (originalEnvironment_.BWS_PROJECT && originalEnvironment_.BWS_ENV) {
const project = config.projects.find(
(p) => p.projectName === originalEnvironment_.BWS_PROJECT
);
if (project) {
const projectId = project.bwsProjectIds[originalEnvironment_.BWS_ENV];
if (projectId) {
process.env.BWS_PROJECT_ID = projectId;
const sourceFile = `.env.secure.${projectId}`;
if (fs.existsSync(sourceFile)) {
const content = fs.readFileSync(sourceFile, 'utf8');
const decrypted = decryptContent(content, process.env.BWS_EPHEMERAL_KEY);
Object.assign(process.env, dotenv.parse(decrypted));
log(
'info',
`Restored environment from ${sourceFile} for ${originalEnvironment_.BWS_ENV} environment`
);
}
}
}
}
}
// Print final environment state
printEnvironmentSummary();
} else {
// For local development, handle single project
// First validate BWS token before prompting
const isValidToken = await validateBwsToken();
if (!isValidToken) {
log('info', 'No valid BWS_ACCESS_TOKEN found, continuing with .env values only');
return;
}
// BWS_PROJECT should already be set by now, but check again just to be safe
if (!process.env.BWS_PROJECT) {
log('warn', 'BWS_PROJECT not set - this should not happen, prompting for selection');
await promptForProject(config.projects);
}
const projectName = process.env.BWS_PROJECT;
const project = config.projects.find((p) => p.projectName === projectName);
if (!project) {
throw new Error(`Project ${projectName} not found in config`);
}
// For local development, prioritize loading the current environment first
const environment = process.env.BWS_ENV || 'local';
const currentProjectId = project.bwsProjectIds[environment];
if (currentProjectId) {
log(
'info',
`Loading secrets for current environment: ${environment} (project ID: ${currentProjectId})`
);
if (!loadedProjectIds.has(currentProjectId)) {
await loadEnvironmentSecrets(currentProjectId, currentProjectId);
loadedProjectIds.add(currentProjectId);
}
// Set the project ID in process.env
process.env.BWS_PROJECT_ID = currentProjectId;
// Load the active environment variables into process.env
const sourceFile = `.env.secure.${currentProjectId}`;
if (fs.existsSync(sourceFile)) {
const content = fs.readFileSync(sourceFile, 'utf8');
const decrypted = decryptContent(content, process.env.BWS_EPHEMERAL_KEY);
// Parse decrypted content but don't override existing env vars
const decryptedVariables = dotenv.parse(decrypted);
for (const key of Object.keys(decryptedVariables)) {
// Only set if not already defined in process.env
if (!(key in process.env)) {
process.env[key] = decryptedVariables[key];
}
}
log('info', `Loaded environment from ${sourceFile} for local development`);
}
} else {
log('warn', `No project ID found for environment ${environment}`);
}
// Then load any other project IDs that might be needed
for (const [env, projectId] of Object.entries(project.bwsProjectIds)) {
if (env !== environment && projectId && !loadedProjectIds.has(projectId)) {
log(
'debug',
`Loading additional secrets for ${projectName} (${env}) project ID: ${projectId}`
);
await loadEnvironmentSecrets(projectId, projectId);
loadedProjectIds.add(projectId);
}
}
// Add environment summary
printEnvironmentSummary();
}
// After all BWS and platform operations, restore original environment variables
for (const [key, value] of Object.entries(originalEnvironment)) {
process.env[key] = value;
}
// If we stored an original BWS_ENV value in debug mode, use it for the file restore
if (process.env.ORIGINAL_BWS_ENV) {
log('debug', `Restoring original BWS_ENV value: ${process.env.ORIGINAL_BWS_ENV}`);
// Find the project
const config = await readConfigFile();
const project = config.projects.find((p) => p.projectName === process.env.BWS_PROJECT);
if (project) {
// Update the .env file with the original environment
await updateEnvironmentBwsSection(project, process.env.ORIGINAL_BWS_ENV);
}
}
// Restore original .env file content
await restoreOriginalEnvironmentFile();
} catch (error) {
log('error', `Failed to setup environment: ${error.message}`);
}
}
// 4) If you also want to demonstrate decrypting from .env.secure, do it now in memory:
function decryptContent(encrypted, encryptionKey) {
const [nonceBase64, authTagBase64, data] = encrypted.split(':');
const nonce = Buffer.from(nonceBase64, 'base64');
const authTag = Buffer.from(authTagBase64, 'base64');
const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(encryptionKey, 'hex'), nonce);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(data, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Make the .env.secure verification optional
if (fs.existsSync('.env.secure') && process.env.BWS_ACCESS_TOKEN && process.env.BWS_EPHEMERAL_KEY) {
try {
const encryptedText = fs.readFileSync('.env.secure', 'utf8');
const checkPlain = decryptContent(encryptedText, process.env.BWS_EPHEMERAL_KEY);
console.log('Verified .env.secure decryption in memory...');
} catch (error) {
console.warn(`[WARN] Decryption from .env.secure failed. Skipping this step: ${error.message}`);
}
}
// Add this function near the other crypto functions
function encryptContent(content, encryptionKey) {
const nonce = crypto.randomBytes(12); // 12 bytes is optimal for GCM
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(encryptionKey, 'hex'), nonce);
let encrypted = cipher.update(content, 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag();
return `${nonce.toString('base64')}:${authTag.toString('base64')}:${encrypted}`;
}
// New function to handle environment-specific secrets
async function loadEnvironmentSecrets(environment, projectId) {
if (!projectId || !environment) {
log('warn', 'Skipping invalid environment config - missing projectId or environment name');
return false;
}
if (!process.env.BWS_ACCESS_TOKEN) {
log('warn', 'Skipping environment - BWS_ACCESS_TOKEN not set');
return false;
}
try {
// More concise logging
log('debug', `Loading secrets for ${projectId}...`);
// 3) Replace "./node_modules/.bin/bws" with getBwsCommand()
const output = execSync(
`${getBwsCommand()} secret list -t ${
process.env.BWS_ACCESS_TOKEN
} ${projectId} --output json`,
{
encoding: 'utf-8',
env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' }
}
);
let bwsSecrets;
try {
bwsSecrets = JSON.parse(output || '[]');
} catch {
log('warn', `No valid secrets found for ${environment}`);
return false;
}
// Create the secure file
const environmentContent = bwsSecrets.map(({ key, value }) => `${key}=${value}`).join('\n');
if (process.env.BWS_EPHEMERAL_KEY && environmentContent) {
const cipherText = encryptContent(environmentContent, process.env.BWS_EPHEMERAL_KEY);
fs.writeFileSync(`.env.secure.${projectId}`, cipherText, { encoding: 'utf-8' });
// Only show detailed counts in debug mode
if (process.env.DEBUG === 'true') {
log('debug', `Created .env.secure.${projectId} with ${bwsSecrets.length} secrets`);
}
return true;
}
return false;
} catch (error) {
if (error?.message?.includes('404 Not Found')) {
log('warn', `Project ${projectId} (${environment}): no secrets found or no access`);
return false;
}
log('warn', `Failed to load secrets for ${environment}`);
if (process.env.DEBUG === 'true') {
if (error?.stdout) {
log('debug', 'stdout:', error.stdout.toString());
}
if (error?.stderr) {
log('debug', 'stderr:', error.stderr.toString());
}
log('debug', 'Error:', error?.message || 'Unknown error');
}
return false;
}
}
// Add this helper function to handle loading the secure env file
function loadSecureEnvironment(environment) {
const secureFile = `.env.secure.${environment}`;
if (fs.existsSync(secureFile)) {
console.log(`Loading ${environment} environment secrets...`);
const encryptedText = fs.readFileSync(secureFile, 'utf8');
const decrypted = decryptContent(encryptedText, process.env.BWS_EPHEMERAL_KEY);
// Load decrypted vars into process.env
for (const line of decrypted.split('\n')) {
const [k, ...rest] = line.split('=');
if (k && rest.length > 0) {
process.env[k.trim()] = rest.join('=').trim();
}
}
console.log(`${environment} environment secrets loaded into process.env`);
} else {
console.warn(
`\u001B[33mWarning: No ${environment} environment secrets found (${secureFile})\u001B[0m`
);
}
}
// Move cleanup function to top level
function cleanupSecureFiles() {
try {
// Clean up all .env.secure.* files and .env.secure
const files = fs.readdirSync(process.cwd());
for (const file of files) {
if (file === '.env.secure' || file.startsWith('.env.secure.')) {
fs.unlinkSync(path.join(process.cwd(), file));
log('debug', `Cleaned up ${file}`);
}
}
} catch (error) {
log('warn', `Error during cleanup: ${error.message}`);
}
}
// Add function to restore original .env file content
async function restoreOriginalEnvironmentFile() {
try {
// Only restore the BWS_ENV value if it was temporarily changed
if (process.env.ORIGINAL_BWS_ENV && process.env.BWS_ENV !== process.env.ORIGINAL_BWS_ENV) {
const environmentPath = path.join(process.cwd(), '.env');
if (fs.existsSync(environmentPath)) {
const content = await fsPromises.readFile(environmentPath, 'utf8');
const lines = content.split('\n');
// Find and update the BWS_ENV line
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
if (line.trim().startsWith('BWS_ENV=') && !line.startsWith('#')) {
// Get comment if any
const parts = line.split('#');
const comment = parts.length > 1 ? `# ${parts.slice(1).join('#').trim()}` : '';
// Replace with original value
lines[index] = `BWS_ENV=${process.env.ORIGINAL_BWS_ENV} ${comment}`;
break;
}
}
// Write back the file with just the BWS_ENV updated
await fsPromises.writeFile(environmentPath, lines.join('\n'));
log('debug', `Restored original BWS_ENV value: ${process.env.ORIGINAL_BWS_ENV}`);
}
} else {
log('debug', 'No environment variables need to be restored');
}
} catch (error) {
log('debug', `Error handling environment variables: ${error.message}`);
}
}
// Update handleUploadCommand function to check for both formats
async function handleUploadCommand() {
// Check if either --clearvars or --clear-vars was passed
const clearVariables =
process.argv.includes('--clearvars') || process.argv.includes('--clear-vars');
// Run the upload-secrets script with clearvars if specified
const uploadScript = path.join(__dirname, 'upload-to-bws', 'upload-secrets.js');
const arguments_ = clearVariables ? ['--clearvars'] : [];
const result = spawnSync('node', [uploadScript, ...arguments_], {
stdio: 'inherit',
env: process.env
});
// Exit with the same status as the upload script
process.exit(result.status);
}
// Main execution
(async () => {
try {
// Ensure BWS is installed before doing anything else
ensureBwsInstalled();
// Parse command line arguments
const arguments_ = process.argv.slice(2);
// Enhanced detection for upload-secrets command that works across package managers
if (arguments_[0] === '--upload-secrets') {
await handleUploadCommand();
return;
}
// Handle the case where -- is used to indicate the start of the command
const commandArgs = arguments_[0] === '--' ? arguments_.slice(1) : arguments_;
// 1. Always load base .env first (but don't override original env vars)
dotenv.config({ override: false });
log('debug', 'Loaded base environment from .env');
// Only perform BWS setup if this is not a nested execution
if (!isNestedExecution) {
// 2. Check if we need to scan for required vars
const requiredVariablesPath = path.join(process.cwd(), 'requiredVars.env');
// Check for special debug flags
const isNetlify = process.env.NETLIFY === 'true';
const isVercel = process.env.VERCEL === '1';
const isDebug = process.env.DEBUG === 'true';
// In debug mode with platform flags, preserve original BWS_ENV if set
if (isDebug && (isNetlify || isVercel) && !process.env.PRESERVE_ENV) {
// Store original value if it exists
if (process.env.BWS_ENV) {
process.env.ORIGINAL_BWS_ENV = process.env.BWS_ENV;
} else if (fs.existsSync('.env')) {
// Try to read from .env file if exists
const environmentContent = fs.readFileSync('.env', 'utf8');
const match = environmentContent.match(/^\s*BWS_ENV\s*=\s*(\w+)/m);
if (match && match[1]) {
process.env.ORIGINAL_BWS_ENV = match[1];
}
}
}
// Only run environment scan for platform builds or debug mode
if (!fs.existsSync(requiredVariablesPath) || isNetlify || isVercel || isDebug) {
log('info', 'Scanning for required variables...');
const scanResult = spawnSync(
'node',
[path.join(__dirname, 'check-vars', 'requiredRuntimeVars.js')],
{
stdio: 'inherit'
}
);
if (scanResult.status !== 0) {
throw new Error('Failed to scan for required variables');
}
}
// 3. Try BWS enhancement if possible
try {
const isValidToken = await validateBwsToken();
if (isValidToken) {
// Single setupEnvironment call that handles both cases
await setupEnvironment({
isPlatformBuild: isNetlify || isVercel
});
// Call map-env-files.js to show decrypted contents if requested
const mapResult = spawnSync(
'node',
[path.join(__dirname, 'update-environments', 'map-env-files.js')],
{
stdio: 'inherit',
env: process.env
}
);
// Restore original variables after mapping
for (const [key, value] of Object.entries(originalEnvironment)) {
process.env[key] = value;
}
} else {
log('info', 'No valid BWS_ACCESS_TOKEN found, continuing with .env values only');
}
} catch (error) {
log('warn', `BWS enhancement failed: ${error.message}`);
}
// 4. Run environment validation regardless
log('info', 'Running environment validation...');
const validator = spawnSync('node', [path.join(__dirname, 'env_validator.js')], {
stdio: 'inherit',
env: process.env
});
// 5. IMPORTANT: Restore original environment variables to ensure CLI-provided vars take precedence
for (const [key, value] of Object.entries(originalEnvironment)) {
process.env[key] = value;
}
// Restore original .env file content
await restoreOriginalEnvironmentFile();
} else {
// For nested executions, just inherit the parent process environment
log('debug', 'Skipping BWS setup in nested execution');
}
// 6. Execute the command
if (commandArgs.length === 0) {
log('warn', 'No command provided to execute');
process.exit(0);
}
const result = spawnSync(commandArgs.join(' '), [], {
stdio: 'inherit',
env: process.env,
shell: true
});
process.exit(result.status);
} catch (error) {
log('error', error.message);
process.exit(1);
}
})();
// Comment out cleanup registrations
process.on('exit', () => {
// Only clean up if this is the root execution
if (!isNestedExecution) {
cleanupSecureFiles();
// We don't want to restore the original .env file as it would remove the project selection
// Instead, the BWS_ENV is handled by restoreOriginalEnvironmentFile() which preserves project options
if (originalEnvironmentFileContent) {
log('debug', 'Skipping complete .env restoration to preserve BWS project selection');
}
}
});
process.on('SIGINT', async () => {
// Only clean up if this is the root execution
if (!isNestedExecution) {
cleanupSecureFiles();
// We don't want to restore the entire original .env file
// The restoreOriginalEnvironmentFile function will only restore the BWS_ENV value if needed
await restoreOriginalEnvironmentFile();
}
process.exit(0);
});
process.on('SIGTERM', async () => {
// Only clean up if this is the root execution
if (!isNestedExecution) {
cleanupSecureFiles();
// We don't want to restore the entire original .env file
// The restoreOriginalEnvironmentFile function will only restore the BWS_ENV value if needed
await restoreOriginalEnvironmentFile();
}
process.exit(0);
});
// Handle uncaught exceptions
process.on('uncaughtException', async (error) => {
console.error('Uncaught Exception:', error);
// Only clean up if this is the root execution
if (!isNestedExecution) {
cleanupSecureFiles();
// We don't want to restore the entire original .env file
// The restoreOriginalEnvironmentFile function will only restore the BWS_ENV value if needed
await restoreOriginalEnvironmentFile();
}
process.exit(1);
});