Skip to main content
Glama
secret-manager.js11.9 kB
import AWS from 'aws-sdk'; import { SecretClient } from '@azure/keyvault-secrets'; import { DefaultAzureCredential } from '@azure/identity'; /** * Universal Secret Management System * Supports environment variables, AWS Secrets Manager, and Azure Key Vault */ export class SecretManager { constructor(config = {}) { this.config = config; this.secretSource = config.secretSource || 'env'; // 'env', 'aws', 'azure' this.cache = new Map(); this.cacheMaxAge = config.cacheMaxAge || 300000; // 5 minutes default this.initializeProviders(); } /** * Safely parse integer from secret value with validation * @param {string} secretValue - Secret value to parse * @param {string} defaultValue - Default value as string * @param {number} min - Minimum allowed value * @param {number} max - Maximum allowed value * @returns {number} Parsed integer value * @private */ _safeParseInt(secretValue, defaultValue, min = 0, max = Number.MAX_SAFE_INTEGER) { const value = parseInt(secretValue || defaultValue); if (isNaN(value) || value < min || value > max) { console.warn( `Invalid integer value from secret: ${secretValue}, using default: ${defaultValue}` ); return parseInt(defaultValue); } return value; } /** * Initialize secret providers based on configuration */ initializeProviders() { if (this.secretSource === 'aws') { this.awsSecretsClient = new AWS.SecretsManager({ region: this.config.awsRegion || process.env.AWS_REGION || 'us-east-1', accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY }); } else if (this.secretSource === 'azure') { const vaultUrl = this.config.azureVaultUrl || process.env.AZURE_KEY_VAULT_URL; if (!vaultUrl) { throw new Error('Azure Key Vault URL is required for Azure secret management'); } this.azureSecretClient = new SecretClient(vaultUrl, new DefaultAzureCredential()); } } /** * Retrieves a secret value from the configured source * @param {string} secretName - Name/key of the secret * @param {object} options - Additional options * @returns {Promise<string|null>} Secret value or null if not found */ async getSecret(secretName, options = {}) { const cacheKey = `${this.secretSource}:${secretName}`; // Check cache first (unless forced refresh) if (!options.refresh && this.cache.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (Date.now() - cached.timestamp < this.cacheMaxAge) { return cached.value; } } let secretValue = null; try { switch (this.secretSource) { case 'env': secretValue = await this.getFromEnvironment(secretName); break; case 'aws': secretValue = await this.getFromAWS(secretName); break; case 'azure': secretValue = await this.getFromAzure(secretName); break; default: throw new Error(`Unsupported secret source: ${this.secretSource}`); } // Cache the result if (secretValue !== null) { this.cache.set(cacheKey, { value: secretValue, timestamp: Date.now() }); } return secretValue; } catch (error) { console.error( `Failed to retrieve secret '${secretName}' from ${this.secretSource}:`, error.message ); // Fallback to environment variable if primary source fails if (this.secretSource !== 'env') { console.warn(`Falling back to environment variable for secret '${secretName}'`); return await this.getFromEnvironment(secretName); } return null; } } /** * Retrieves secret from environment variables * @param {string} secretName - Environment variable name * @returns {Promise<string|null>} Secret value */ async getFromEnvironment(secretName) { return process.env[secretName] || null; } /** * Retrieves secret from AWS Secrets Manager * @param {string} secretName - Secret ARN or name * @returns {Promise<string|null>} Secret value */ async getFromAWS(secretName) { if (!this.awsSecretsClient) { throw new Error('AWS Secrets Manager client not initialized'); } const params = { SecretId: secretName, VersionStage: 'AWSCURRENT' }; const result = await this.awsSecretsClient.getSecretValue(params).promise(); if (result.SecretString) { // Handle both plain string and JSON secrets try { const secretObj = JSON.parse(result.SecretString); return secretObj; } catch { return result.SecretString; } } return null; } /** * Retrieves secret from Azure Key Vault * @param {string} secretName - Secret name (environment variable format) * @returns {Promise<string|null>} Secret value */ async getFromAzure(secretName) { if (!this.azureSecretClient) { throw new Error('Azure Key Vault client not initialized'); } // Convert environment variable format to Azure Key Vault format // Environment: SQL_SERVER_HOST -> Azure: SQL-SERVER-HOST const azureSecretName = this.convertToAzureSecretName(secretName); try { const secret = await this.azureSecretClient.getSecret(azureSecretName); return secret.value || null; } catch (error) { // If the converted name fails, try the original name as fallback if (azureSecretName !== secretName) { console.warn( `Azure secret '${azureSecretName}' not found, trying original name '${secretName}'` ); try { const secret = await this.azureSecretClient.getSecret(secretName); return secret.value || null; } catch (fallbackError) { // Log both attempts for debugging throw new Error( `Azure Key Vault secret not found: '${azureSecretName}' or '${secretName}'. ` + `Original error: ${error.message}, Fallback error: ${fallbackError.message}` ); } } throw error; } } /** * Converts environment variable names to Azure Key Vault compatible format * @param {string} envVarName - Environment variable name (e.g., SQL_SERVER_HOST) * @returns {string} Azure Key Vault secret name (e.g., SQL-SERVER-HOST) */ convertToAzureSecretName(envVarName) { // Azure Key Vault secret names: // - Can contain letters, numbers, and hyphens // - Cannot contain underscores // - Are case-insensitive // - Must be 1-127 characters return envVarName.replace(/_/g, '-'); } /** * Retrieves database connection configuration with secrets resolution * @returns {Promise<object>} Database configuration object */ async getDatabaseConfig() { const config = { server: (await this.getSecret('SQL_SERVER_HOST')) || 'localhost', port: this._safeParseInt(await this.getSecret('SQL_SERVER_PORT'), '1433', 1, 65535), database: (await this.getSecret('SQL_SERVER_DATABASE')) || 'master', user: await this.getSecret('SQL_SERVER_USER'), password: await this.getSecret('SQL_SERVER_PASSWORD'), domain: await this.getSecret('SQL_SERVER_DOMAIN'), // Connection options encrypt: (await this.getSecret('SQL_SERVER_ENCRYPT')) === 'true' || false, trustServerCertificate: (await this.getSecret('SQL_SERVER_TRUST_CERT')) === 'true' || true, // Timeout configurations connectionTimeout: this._safeParseInt( await this.getSecret('SQL_SERVER_CONNECT_TIMEOUT_MS'), '10000', 1000, 60000 ), requestTimeout: this._safeParseInt( await this.getSecret('SQL_SERVER_REQUEST_TIMEOUT_MS'), '30000', 5000, 300000 ), maxRetries: this._safeParseInt(await this.getSecret('SQL_SERVER_MAX_RETRIES'), '3', 0, 10), retryDelay: this._safeParseInt( await this.getSecret('SQL_SERVER_RETRY_DELAY_MS'), '1000', 100, 10000 ), // Pool settings poolMax: this._safeParseInt(await this.getSecret('SQL_SERVER_POOL_MAX'), '10', 1, 100), poolMin: this._safeParseInt(await this.getSecret('SQL_SERVER_POOL_MIN'), '0', 0, 50), poolIdleTimeout: this._safeParseInt( await this.getSecret('SQL_SERVER_POOL_IDLE_TIMEOUT_MS'), '30000', 1000, 300000 ), // Security settings readOnlyMode: (await this.getSecret('SQL_SERVER_READ_ONLY')) !== 'false', // Default: true allowDestructiveOperations: (await this.getSecret('SQL_SERVER_ALLOW_DESTRUCTIVE_OPERATIONS')) === 'true', allowSchemaChanges: (await this.getSecret('SQL_SERVER_ALLOW_SCHEMA_CHANGES')) === 'true', // Logging and monitoring logLevel: (await this.getSecret('SQL_SERVER_LOG_LEVEL')) || 'info' }; return config; } /** * Retrieves multiple secrets in parallel * @param {string[]} secretNames - Array of secret names * @returns {Promise<object>} Object mapping secret names to values */ async getSecrets(secretNames) { const promises = secretNames.map(async name => { const value = await this.getSecret(name); return { name, value }; }); const results = await Promise.all(promises); const secretMap = {}; for (const { name, value } of results) { secretMap[name] = value; } return secretMap; } /** * Clears the secret cache */ clearCache() { this.cache.clear(); } /** * Health check for secret providers * @returns {Promise<object>} Health status */ async healthCheck() { const health = { source: this.secretSource, status: 'unknown', details: {} }; try { switch (this.secretSource) { case 'env': health.status = 'healthy'; health.details.environment = 'accessible'; break; case 'aws': if (this.awsSecretsClient) { // Test with a dummy call to validate credentials try { await this.awsSecretsClient.listSecrets({ MaxResults: 1 }).promise(); health.status = 'healthy'; health.details.aws = 'authenticated'; } catch (error) { health.status = 'unhealthy'; health.details.aws = `authentication failed: ${error.message}`; } } else { health.status = 'unhealthy'; health.details.aws = 'client not initialized'; } break; case 'azure': if (this.azureSecretClient) { try { // Test with a simple operation await this.azureSecretClient.listPropertiesOfSecrets().next(); health.status = 'healthy'; health.details.azure = 'authenticated'; } catch (error) { health.status = 'unhealthy'; health.details.azure = `authentication failed: ${error.message}`; } } else { health.status = 'unhealthy'; health.details.azure = 'client not initialized'; } break; } } catch (error) { health.status = 'unhealthy'; health.details.error = error.message; } return health; } /** * Creates a masked version of configuration for logging * @param {object} config - Configuration object * @returns {object} Masked configuration */ static maskSecrets(config) { const masked = { ...config }; const secretFields = ['password', 'user', 'connectionString']; for (const field of secretFields) { if (masked[field] && typeof masked[field] === 'string') { masked[field] = '***MASKED***'; } } return masked; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/egarcia74/warp-sql-server-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server