Root cause analysis and fixes: 1. **MCP_MODE environment variable tests** - Issue: Tests were checking env vars after exec process replacement - Fix: Test actual HTTP server behavior instead of env vars - Changed tests to verify health endpoint responds in HTTP mode 2. **NODE_DB_PATH configuration tests** - Issue: Tests expected env var output but got initialization logs - Fix: Check process environment via /proc/1/environ - Added proper async handling for container startup 3. **Permission handling tests** - Issue: BusyBox sleep syntax and timing race conditions - Fix: Use detached containers with proper wait times - Check permissions after entrypoint completes 4. **Implementation improvements** - Export NODE_DB_PATH in entrypoint for visibility - Preserve env vars when switching to nodejs user - Add debug output option in n8n-mcp wrapper - Handle NODE_DB_PATH case preservation in parse-config.js 5. **Test infrastructure** - Created test-helpers.ts with proper async utilities - Use health checks instead of arbitrary sleep times - Test actual functionality rather than implementation details These changes ensure tests verify the actual behavior (server running, health endpoint responding) rather than checking internal implementation details that aren't accessible after process replacement. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
192 lines
5.7 KiB
JavaScript
192 lines
5.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Parse JSON config file and output shell-safe export commands
|
|
* Only outputs variables that aren't already set in environment
|
|
*
|
|
* Security: Uses safe quoting without any shell execution
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
|
|
// Debug logging support
|
|
const DEBUG = process.env.DEBUG_CONFIG === 'true';
|
|
|
|
function debugLog(message) {
|
|
if (DEBUG) {
|
|
process.stderr.write(`[parse-config] ${message}\n`);
|
|
}
|
|
}
|
|
|
|
const configPath = process.argv[2] || '/app/config.json';
|
|
debugLog(`Using config path: ${configPath}`);
|
|
|
|
// Dangerous environment variables that should never be set
|
|
const DANGEROUS_VARS = new Set([
|
|
'PATH', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'LD_AUDIT',
|
|
'BASH_ENV', 'ENV', 'CDPATH', 'IFS', 'PS1', 'PS2', 'PS3', 'PS4',
|
|
'SHELL', 'BASH_FUNC', 'SHELLOPTS', 'GLOBIGNORE',
|
|
'PERL5LIB', 'PYTHONPATH', 'NODE_PATH', 'RUBYLIB'
|
|
]);
|
|
|
|
/**
|
|
* Sanitize a key name for use as environment variable
|
|
* Converts to uppercase and replaces invalid chars with underscore
|
|
*/
|
|
function sanitizeKey(key) {
|
|
// Convert to string and handle edge cases
|
|
const keyStr = String(key || '').trim();
|
|
|
|
if (!keyStr) {
|
|
return 'EMPTY_KEY';
|
|
}
|
|
|
|
// Special handling for NODE_DB_PATH to preserve exact casing
|
|
if (keyStr === 'NODE_DB_PATH') {
|
|
return 'NODE_DB_PATH';
|
|
}
|
|
|
|
const sanitized = keyStr
|
|
.toUpperCase()
|
|
.replace(/[^A-Z0-9]+/g, '_')
|
|
.replace(/^_+|_+$/g, '') // Trim underscores
|
|
.replace(/^(\d)/, '_$1'); // Prefix with _ if starts with number
|
|
|
|
// If sanitization results in empty string, use a default
|
|
return sanitized || 'EMPTY_KEY';
|
|
}
|
|
|
|
/**
|
|
* Safely quote a string for shell use
|
|
* This follows POSIX shell quoting rules
|
|
*/
|
|
function shellQuote(str) {
|
|
// Remove null bytes which are not allowed in environment variables
|
|
str = str.replace(/\x00/g, '');
|
|
|
|
// Always use single quotes for consistency and safety
|
|
// Single quotes protect everything except other single quotes
|
|
return "'" + str.replace(/'/g, "'\"'\"'") + "'";
|
|
}
|
|
|
|
try {
|
|
if (!fs.existsSync(configPath)) {
|
|
debugLog(`Config file not found at: ${configPath}`);
|
|
process.exit(0); // Silent exit if no config file
|
|
}
|
|
|
|
let configContent;
|
|
let config;
|
|
|
|
try {
|
|
configContent = fs.readFileSync(configPath, 'utf8');
|
|
debugLog(`Read config file, size: ${configContent.length} bytes`);
|
|
} catch (readError) {
|
|
// Silent exit on read errors
|
|
debugLog(`Error reading config: ${readError.message}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
try {
|
|
config = JSON.parse(configContent);
|
|
debugLog(`Parsed config with ${Object.keys(config).length} top-level keys`);
|
|
} catch (parseError) {
|
|
// Silent exit on invalid JSON
|
|
debugLog(`Error parsing JSON: ${parseError.message}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Validate config is an object
|
|
if (typeof config !== 'object' || config === null || Array.isArray(config)) {
|
|
// Silent exit on invalid config structure
|
|
process.exit(0);
|
|
}
|
|
|
|
// Convert nested objects to flat environment variables
|
|
const flattenConfig = (obj, prefix = '', depth = 0) => {
|
|
const result = {};
|
|
|
|
// Prevent infinite recursion
|
|
if (depth > 10) {
|
|
return result;
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const sanitizedKey = sanitizeKey(key);
|
|
|
|
// Skip if sanitization resulted in EMPTY_KEY (indicating invalid key)
|
|
if (sanitizedKey === 'EMPTY_KEY') {
|
|
debugLog(`Skipping key '${key}': invalid key name`);
|
|
continue;
|
|
}
|
|
|
|
const envKey = prefix ? `${prefix}_${sanitizedKey}` : sanitizedKey;
|
|
|
|
// Skip if key is too long
|
|
if (envKey.length > 255) {
|
|
debugLog(`Skipping key '${envKey}': too long (${envKey.length} chars)`);
|
|
continue;
|
|
}
|
|
|
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
// Recursively flatten nested objects
|
|
Object.assign(result, flattenConfig(value, envKey, depth + 1));
|
|
} else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
// Only include if not already set in environment
|
|
if (!process.env[envKey]) {
|
|
let stringValue = String(value);
|
|
|
|
// Handle special JavaScript number values
|
|
if (typeof value === 'number') {
|
|
if (!isFinite(value)) {
|
|
if (value === Infinity) {
|
|
stringValue = 'Infinity';
|
|
} else if (value === -Infinity) {
|
|
stringValue = '-Infinity';
|
|
} else if (isNaN(value)) {
|
|
stringValue = 'NaN';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Skip if value is too long
|
|
if (stringValue.length <= 32768) {
|
|
result[envKey] = stringValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// Output shell-safe export commands
|
|
const flattened = flattenConfig(config);
|
|
const exports = [];
|
|
|
|
for (const [key, value] of Object.entries(flattened)) {
|
|
// Validate key name (alphanumeric and underscore only)
|
|
if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) {
|
|
continue; // Skip invalid variable names
|
|
}
|
|
|
|
// Skip dangerous variables
|
|
if (DANGEROUS_VARS.has(key) || key.startsWith('BASH_FUNC_')) {
|
|
debugLog(`Warning: Ignoring dangerous variable: ${key}`);
|
|
process.stderr.write(`Warning: Ignoring dangerous variable: ${key}\n`);
|
|
continue;
|
|
}
|
|
|
|
// Safely quote the value
|
|
const quotedValue = shellQuote(value);
|
|
exports.push(`export ${key}=${quotedValue}`);
|
|
}
|
|
|
|
// Use process.stdout.write to ensure output goes to stdout
|
|
if (exports.length > 0) {
|
|
process.stdout.write(exports.join('\n') + '\n');
|
|
}
|
|
|
|
} catch (error) {
|
|
// Silent fail - don't break the container startup
|
|
process.exit(0);
|
|
} |