@accounter/server
Version:
The test suite is split into three Vitest projects for efficiency and isolation:
98 lines (87 loc) • 2.73 kB
text/typescript
import { readFile, writeFile, rename, unlink } from 'fs/promises';
/**
* Write or update an environment variable in a .env file
*
* Uses atomic write pattern (temp file + rename) to prevent corruption
* if process crashes during write.
*
* ⚠️ **SECURITY WARNING**: This function stores values in PLAINTEXT.
* Never use for production secrets. Use environment variables,
* secret managers (AWS Secrets Manager, Vault), or encrypted files.
*
* **Thread Safety**: Not safe for concurrent writes to the same file.
* Multiple processes writing simultaneously may cause race conditions.
*
* **Format Assumptions**: Assumes KEY=VALUE format without quotes or inline comments.
*
* @param filePath - Path to the .env file
* @param key - Environment variable key (uppercase recommended)
* @param value - Environment variable value (stored as-is)
*
* @example
* ```typescript
* // Create new variable
* await writeEnvVar('.env', 'DATABASE_URL', 'postgresql://localhost/mydb');
*
* // Update existing variable
* await writeEnvVar('.env', 'DATABASE_URL', 'postgresql://localhost/newdb');
*
* // Result is idempotent
* await writeEnvVar('.env', 'API_KEY', 'abc123');
* await writeEnvVar('.env', 'API_KEY', 'abc123'); // No change
* ```
*/
export async function writeEnvVar(
filePath: string,
key: string,
value: string,
): Promise<void> {
let content = '';
try {
content = await readFile(filePath, 'utf-8');
} catch (error) {
// File doesn't exist, will create it
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error;
}
}
const lines = content.split('\n');
const keyPattern = new RegExp(`^${escapeRegExp(key)}=`);
let found = false;
// Remove trailing empty string from split (if content ended with \n)
if (lines.length > 0 && lines[lines.length - 1] === '') {
lines.pop();
}
const updatedLines = lines.map(line => {
if (keyPattern.test(line)) {
found = true;
return `${key}=${value}`;
}
return line;
});
if (!found) {
updatedLines.push(`${key}=${value}`);
}
// Join with newlines and add single trailing newline
const newContent = updatedLines.join('\n') + '\n';
// Atomic write: temp file + rename
const tempPath = `${filePath}.tmp.${process.pid}`;
try {
await writeFile(tempPath, newContent, 'utf-8');
await rename(tempPath, filePath);
} catch (error) {
// Clean up temp file on error
try {
await unlink(tempPath);
} catch {
// Ignore cleanup errors
}
throw error;
}
}
/**
* Escape special regex characters in a string
*/
function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}