/**
* Docker Secrets Manager
* Handles secure loading of secrets from Docker secrets and environment variables
*/
import fs from 'fs';
import path from 'path';
/**
* Secrets Manager for handling Docker secrets and environment variables
*/
export class SecretsManager {
constructor(options = {}) {
this.config = {
secretsPath: options.secretsPath || '/run/secrets',
fallbackToEnv: options.fallbackToEnv !== false,
enableCaching: options.enableCaching !== false,
cacheTimeout: options.cacheTimeout || 300000, // 5 minutes
enableLogging: options.enableLogging !== false,
secretsMapping: options.secretsMapping || {
// Map secret names to environment variable names
'anthropic_api_key': 'ANTHROPIC_API_KEY',
'hf_token': 'HF_TOKEN',
'comfyui_auth': 'COMFYUI_AUTH',
'mcp_auth_token': 'MCP_AUTH_TOKEN'
},
...options
};
// Cache for loaded secrets
this.cache = new Map();
this.cacheTimestamps = new Map();
}
/**
* Load a secret from Docker secrets or environment variable
* @param {string} secretName - Name of the secret
* @param {Object} options - Loading options
* @returns {string|null} The secret value or null if not found
*/
async loadSecret(secretName, options = {}) {
// Check cache first
if (this.config.enableCaching && this.cache.has(secretName)) {
const timestamp = this.cacheTimestamps.get(secretName);
if (Date.now() - timestamp < this.config.cacheTimeout) {
return this.cache.get(secretName);
}
}
let secretValue = null;
// Try to load from Docker secrets first
try {
secretValue = await this.loadFromDockerSecret(secretName);
if (secretValue) {
if (this.config.enableLogging) {
console.log(`✅ Loaded secret '${secretName}' from Docker secrets`);
}
}
} catch (error) {
if (this.config.enableLogging) {
console.warn(`⚠️ Could not load secret '${secretName}' from Docker: ${error.message}`);
}
}
// Fall back to environment variable if enabled and not found in Docker secrets
if (!secretValue && this.config.fallbackToEnv) {
const envName = this.config.secretsMapping[secretName] || secretName.toUpperCase();
secretValue = process.env[envName];
if (secretValue) {
if (this.config.enableLogging) {
console.log(`✅ Loaded secret '${secretName}' from environment variable ${envName}`);
}
}
}
// Try custom loader if provided
if (!secretValue && options.customLoader) {
try {
secretValue = await options.customLoader(secretName);
} catch (error) {
if (this.config.enableLogging) {
console.error(`❌ Custom loader failed for secret '${secretName}': ${error.message}`);
}
}
}
// Cache the result
if (secretValue && this.config.enableCaching) {
this.cache.set(secretName, secretValue);
this.cacheTimestamps.set(secretName, Date.now());
}
return secretValue;
}
/**
* Load secret from Docker secrets file
* @param {string} secretName - Name of the secret file
* @returns {string|null} The secret value or null if not found
*/
async loadFromDockerSecret(secretName) {
const secretPath = path.join(this.config.secretsPath, secretName);
try {
// Check if the file exists
await fs.promises.access(secretPath, fs.constants.R_OK);
// Read the secret file
const secretValue = await fs.promises.readFile(secretPath, 'utf8');
// Trim whitespace and newlines
return secretValue.trim();
} catch (error) {
if (error.code === 'ENOENT') {
// File doesn't exist - this is expected in many cases
return null;
}
throw error;
}
}
/**
* Load multiple secrets at once
* @param {string[]} secretNames - Array of secret names
* @returns {Object} Object with secret names as keys and values
*/
async loadSecrets(secretNames) {
const secrets = {};
for (const name of secretNames) {
const value = await this.loadSecret(name);
if (value) {
secrets[name] = value;
}
}
return secrets;
}
/**
* Load all configured secrets
* @returns {Object} Object with all loaded secrets
*/
async loadAllSecrets() {
const secretNames = Object.keys(this.config.secretsMapping);
return this.loadSecrets(secretNames);
}
/**
* Clear the secrets cache
*/
clearCache() {
this.cache.clear();
this.cacheTimestamps.clear();
}
/**
* Validate that required secrets are present
* @param {string[]} requiredSecrets - Array of required secret names
* @returns {Object} { valid: boolean, missing: string[] }
*/
async validateSecrets(requiredSecrets) {
const missing = [];
for (const secretName of requiredSecrets) {
const value = await this.loadSecret(secretName);
if (!value) {
missing.push(secretName);
}
}
return {
valid: missing.length === 0,
missing
};
}
/**
* Safe secret comparison (constant time to prevent timing attacks)
* @param {string} secret1 - First secret
* @param {string} secret2 - Second secret
* @returns {boolean} True if secrets match
*/
static safeCompare(secret1, secret2) {
if (typeof secret1 !== 'string' || typeof secret2 !== 'string') {
return false;
}
if (secret1.length !== secret2.length) {
return false;
}
let result = 0;
for (let i = 0; i < secret1.length; i++) {
result |= secret1.charCodeAt(i) ^ secret2.charCodeAt(i);
}
return result === 0;
}
/**
* Mask a secret for logging (show only first and last characters)
* @param {string} secret - The secret to mask
* @returns {string} Masked secret
*/
static maskSecret(secret) {
if (!secret || secret.length < 4) {
return '***';
}
const firstChar = secret[0];
const lastChar = secret[secret.length - 1];
const maskedLength = Math.max(3, secret.length - 2);
return `${firstChar}${'*'.repeat(maskedLength)}${lastChar}`;
}
}
/**
* Environment-specific secret loader
*/
export class EnvironmentSecretsLoader {
constructor() {
this.environment = process.env.NODE_ENV || 'production';
}
/**
* Load secrets based on environment
*/
async loadForEnvironment() {
const manager = new SecretsManager();
if (this.environment === 'production') {
// In production, prefer Docker secrets
return manager.loadAllSecrets();
} else if (this.environment === 'development') {
// In development, load from .env file or environment
return this.loadFromEnvFile();
} else {
// In test, use mock secrets
return this.loadMockSecrets();
}
}
/**
* Load secrets from .env file (development)
*/
async loadFromEnvFile() {
const envPath = path.join(process.cwd(), '.env');
try {
const envContent = await fs.promises.readFile(envPath, 'utf8');
const secrets = {};
envContent.split('\n').forEach(line => {
const [key, value] = line.split('=');
if (key && value) {
secrets[key.trim()] = value.trim();
}
});
return secrets;
} catch (error) {
console.warn('Could not load .env file:', error.message);
return {};
}
}
/**
* Load mock secrets for testing
*/
loadMockSecrets() {
return {
anthropic_api_key: 'mock_anthropic_key',
hf_token: 'mock_hf_token',
comfyui_auth: 'mock_auth_token'
};
}
}
/**
* Create a singleton secrets manager instance
*/
let secretsManagerInstance = null;
export function getSecretsManager(options = {}) {
if (!secretsManagerInstance) {
secretsManagerInstance = new SecretsManager(options);
}
return secretsManagerInstance;
}
/**
* Express middleware for API key authentication using secrets
*/
export function createAuthMiddleware(options = {}) {
const secretsManager = getSecretsManager(options);
return async (req, res, next) => {
// Get API key from header or query parameter
const apiKey = req.headers['x-api-key'] ||
req.headers['authorization']?.replace('Bearer ', '') ||
req.query.api_key;
if (!apiKey) {
return res.status(401).json({
error: 'Authentication required',
message: 'Please provide an API key'
});
}
// Load the expected API key from secrets
const expectedKey = await secretsManager.loadSecret('mcp_auth_token');
if (!expectedKey) {
console.error('No authentication token configured');
return res.status(500).json({
error: 'Server configuration error',
message: 'Authentication not properly configured'
});
}
// Safe comparison to prevent timing attacks
if (!SecretsManager.safeCompare(apiKey, expectedKey)) {
return res.status(403).json({
error: 'Invalid API key',
message: 'The provided API key is invalid'
});
}
// Authentication successful
req.authenticated = true;
next();
};
}