Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée. Co-authored-by: Cursor <cursoragent@cursor.com>
363 lines
11 KiB
JavaScript
363 lines
11 KiB
JavaScript
/**
|
|
* Memento MCP Server - Configuration Management
|
|
*
|
|
* Centralized configuration with validation and defaults.
|
|
* Validates all required environment variables on startup.
|
|
*/
|
|
|
|
/**
|
|
* Parse boolean from string or value
|
|
*/
|
|
function parseBoolean(value, defaultValue = false) {
|
|
if (value === undefined || value === null || value === '') {
|
|
return defaultValue;
|
|
}
|
|
if (typeof value === 'boolean') return value;
|
|
const str = String(value).toLowerCase();
|
|
return ['true', '1', 'yes', 'on'].includes(str);
|
|
}
|
|
|
|
/**
|
|
* Parse integer with default
|
|
*/
|
|
function parseInt(value, defaultValue, min, max) {
|
|
if (value === undefined || value === null || value === '') {
|
|
return defaultValue;
|
|
}
|
|
const parsed = Number.parseInt(value, 10);
|
|
if (Number.isNaN(parsed)) return defaultValue;
|
|
if (min !== undefined && parsed < min) return min;
|
|
if (max !== undefined && parsed > max) return max;
|
|
return parsed;
|
|
}
|
|
|
|
/**
|
|
* Parse array from comma-separated string
|
|
*/
|
|
function parseArray(value, defaultValue = []) {
|
|
if (!value) return defaultValue;
|
|
if (Array.isArray(value)) return value;
|
|
return String(value).split(',').map((s) => s.trim()).filter(Boolean);
|
|
}
|
|
|
|
/**
|
|
* Get environment variable with fallback
|
|
*/
|
|
function env(key, fallback = '') {
|
|
return process.env[key] || fallback;
|
|
}
|
|
|
|
/**
|
|
* Validate database URL format
|
|
*/
|
|
function validateDatabaseUrl(url) {
|
|
if (!url) {
|
|
return { valid: false, error: 'DATABASE_URL is required' };
|
|
}
|
|
|
|
const isValid =
|
|
url.startsWith('postgresql://') ||
|
|
url.startsWith('postgres://') ||
|
|
url.startsWith('file:') ||
|
|
url.includes('.db') ||
|
|
url.includes('.sqlite');
|
|
|
|
if (!isValid) {
|
|
return {
|
|
valid: false,
|
|
error: 'DATABASE_URL must be a valid PostgreSQL or SQLite connection string',
|
|
};
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Validate port number
|
|
*/
|
|
function validatePort(port) {
|
|
if (port < 1 || port > 65535) {
|
|
return { valid: false, error: `PORT must be between 1 and 65535, got ${port}` };
|
|
}
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Validate log level
|
|
*/
|
|
function validateLogLevel(level) {
|
|
const validLevels = ['debug', 'info', 'warn', 'error', 'silent'];
|
|
if (!validLevels.includes(level)) {
|
|
return {
|
|
valid: false,
|
|
error: `LOG_LEVEL must be one of: ${validLevels.join(', ')}`,
|
|
};
|
|
}
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Validate timeout values
|
|
*/
|
|
function validateTimeout(timeout, name) {
|
|
const min = 1000; // 1 second minimum
|
|
const max = 300000; // 5 minutes maximum
|
|
|
|
if (timeout < min || timeout > max) {
|
|
return {
|
|
valid: false,
|
|
error: `${name} must be between ${min}ms and ${max}ms, got ${timeout}ms`,
|
|
};
|
|
}
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Configuration object with validation
|
|
*/
|
|
export const config = {
|
|
// Server
|
|
port: parseInt(env('PORT', '3001'), 3001, 1, 65535),
|
|
nodeEnv: env('NODE_ENV', 'development'),
|
|
|
|
// Database
|
|
databaseUrl: env('DATABASE_URL', ''),
|
|
isPostgres:
|
|
env('DATABASE_URL', '').startsWith('postgresql://') ||
|
|
env('DATABASE_URL', '').startsWith('postgres://'),
|
|
connectionLimit: parseInt(env('DB_CONNECTION_LIMIT', '10'), 10, 1, 100),
|
|
poolTimeout: parseInt(env('DB_POOL_TIMEOUT', '10'), 10, 1, 60),
|
|
|
|
// Application
|
|
appBaseUrl: env('APP_BASE_URL', 'http://localhost:3000'),
|
|
userId: env('USER_ID', null), // Optional user filter
|
|
|
|
// Authentication
|
|
requireAuth: parseBoolean(env('MCP_REQUIRE_AUTH'), false),
|
|
staticApiKey: env('MCP_API_KEY', null),
|
|
|
|
// Logging
|
|
logLevel: env('MCP_LOG_LEVEL', 'info').toLowerCase(),
|
|
logToFile: parseBoolean(env('MCP_LOG_TO_FILE'), false),
|
|
|
|
// Performance
|
|
requestTimeout: parseInt(env('MCP_REQUEST_TIMEOUT', '30000'), 30000, 1000, 300000),
|
|
rateLimit: parseInt(env('MCP_RATE_LIMIT', '100'), 100, 1, 10000),
|
|
rateLimitWindow: parseInt(env('MCP_RATE_LIMIT_WINDOW', '60000'), 60000, 1000, 3600000),
|
|
|
|
// Session management
|
|
maxSessions: parseInt(env('MCP_MAX_SESSIONS', '500'), 500, 10, 10000),
|
|
sessionTtl: parseInt(env('MCP_SESSION_TTL', '3600000'), 3600000, 60000, 86400000),
|
|
sessionCleanupInterval: parseInt(env('MCP_SESSION_CLEANUP_INTERVAL', '300000'), 300000, 60000, 3600000),
|
|
|
|
// Caching
|
|
enableCache: parseBoolean(env('MCP_ENABLE_CACHE'), true),
|
|
cacheTtl: parseInt(env('MCP_CACHE_TTL', '60000'), 60000, 0, 3600000),
|
|
cacheMaxSize: parseInt(env('MCP_CACHE_MAX_SIZE', '1000'), 1000, 100, 10000),
|
|
|
|
// Features
|
|
enableMetrics: parseBoolean(env('MCP_ENABLE_METRICS'), true),
|
|
enableAuditLog: parseBoolean(env('MCP_ENABLE_AUDIT_LOG'), true),
|
|
enableTools: parseArray(env('MCP_ENABLE_TOOLS'), null), // null = all tools enabled
|
|
disableTools: parseArray(env('MCP_DISABLE_TOOLS'), []),
|
|
|
|
// Security
|
|
maxRequestSize: parseInt(env('MCP_MAX_REQUEST_SIZE', '10485760'), 10485760, 1024, 104857600), // 10MB default
|
|
maxResponseSize: parseInt(env('MCP_MAX_RESPONSE_SIZE', '52428800'), 52428800, 1024, 524288000), // 50MB default
|
|
allowedOrigins: parseArray(env('MCP_ALLOWED_ORIGINS'), '*'),
|
|
|
|
// Observability
|
|
metricsPath: env('MCP_METRICS_PATH', '/metrics'),
|
|
healthPath: env('MCP_HEALTH_PATH', '/health'),
|
|
debugPath: env('MCP_DEBUG_PATH', '/debug'),
|
|
|
|
// Timeouts
|
|
databaseQueryTimeout: parseInt(env('MCP_DB_QUERY_TIMEOUT', '30000'), 30000, 1000, 120000),
|
|
toolExecutionTimeout: parseInt(env('MCP_TOOL_TIMEOUT', '60000'), 60000, 5000, 300000),
|
|
};
|
|
|
|
/**
|
|
* Validate all configuration values
|
|
* Returns array of validation errors (empty if valid)
|
|
*/
|
|
export function validateConfig() {
|
|
const errors = [];
|
|
|
|
// Required fields
|
|
if (!config.databaseUrl) {
|
|
errors.push({
|
|
key: 'DATABASE_URL',
|
|
message: 'DATABASE_URL is required',
|
|
critical: true,
|
|
});
|
|
} else {
|
|
const dbValidation = validateDatabaseUrl(config.databaseUrl);
|
|
if (!dbValidation.valid) {
|
|
errors.push({ key: 'DATABASE_URL', message: dbValidation.error, critical: true });
|
|
}
|
|
}
|
|
|
|
// Port validation (check raw value before clamping)
|
|
const rawPort = Number.parseInt(env('PORT', '3001'), 10);
|
|
const portValidation = validatePort(Number.isNaN(rawPort) ? config.port : rawPort);
|
|
if (!portValidation.valid) {
|
|
errors.push({ key: 'PORT', message: portValidation.error, critical: true });
|
|
}
|
|
|
|
// Log level validation
|
|
const logLevelValidation = validateLogLevel(config.logLevel);
|
|
if (!logLevelValidation.valid) {
|
|
errors.push({ key: 'MCP_LOG_LEVEL', message: logLevelValidation.error, critical: false });
|
|
}
|
|
|
|
// Timeout validations
|
|
const requestTimeoutValidation = validateTimeout(config.requestTimeout, 'REQUEST_TIMEOUT');
|
|
if (!requestTimeoutValidation.valid) {
|
|
errors.push({
|
|
key: 'MCP_REQUEST_TIMEOUT',
|
|
message: requestTimeoutValidation.error,
|
|
critical: false,
|
|
});
|
|
}
|
|
|
|
const dbTimeoutValidation = validateTimeout(config.databaseQueryTimeout, 'DB_QUERY_TIMEOUT');
|
|
if (!dbTimeoutValidation.valid) {
|
|
errors.push({
|
|
key: 'MCP_DB_QUERY_TIMEOUT',
|
|
message: dbTimeoutValidation.error,
|
|
critical: false,
|
|
});
|
|
}
|
|
|
|
// Auth configuration
|
|
if (config.requireAuth && !config.staticApiKey) {
|
|
// Warning: auth required but no static key set
|
|
// This is OK - we'll use database-stored keys
|
|
// Just log a warning in development
|
|
if (config.nodeEnv === 'development') {
|
|
errors.push({
|
|
key: 'MCP_REQUIRE_AUTH',
|
|
message: 'Auth is required but no MCP_API_KEY is set. Database API keys will be used.',
|
|
critical: false,
|
|
level: 'warning',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for conflicting tool enable/disable
|
|
if (config.enableTools && config.disableTools.length > 0) {
|
|
const conflicts = config.enableTools.filter((t) => config.disableTools.includes(t));
|
|
if (conflicts.length > 0) {
|
|
errors.push({
|
|
key: 'MCP_ENABLE_TOOLS / MCP_DISABLE_TOOLS',
|
|
message: `Tools both enabled and disabled: ${conflicts.join(', ')}. Disabled takes precedence.`,
|
|
critical: false,
|
|
level: 'warning',
|
|
});
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Get configuration for display (sanitized)
|
|
* Removes sensitive values like API keys and database URLs
|
|
*/
|
|
export function getPublicConfig() {
|
|
return {
|
|
port: config.port,
|
|
nodeEnv: config.nodeEnv,
|
|
isPostgres: config.isPostgres,
|
|
appBaseUrl: config.appBaseUrl,
|
|
userId: config.userId || null,
|
|
requireAuth: config.requireAuth,
|
|
hasStaticKey: Boolean(config.staticApiKey),
|
|
logLevel: config.logLevel,
|
|
requestTimeout: config.requestTimeout,
|
|
rateLimit: config.rateLimit,
|
|
maxSessions: config.maxSessions,
|
|
sessionTtl: config.sessionTtl,
|
|
enableCache: config.enableCache,
|
|
cacheTtl: config.cacheTtl,
|
|
enableMetrics: config.enableMetrics,
|
|
enableAuditLog: config.enableAuditLog,
|
|
enabledToolCount: config.enableTools?.length || 'all',
|
|
disabledToolCount: config.disableTools.length,
|
|
maxRequestSize: config.maxRequestSize,
|
|
allowedOrigins: config.allowedOrigins,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get database URL for logging (sanitized)
|
|
*/
|
|
export function getSafeDatabaseUrl() {
|
|
if (!config.databaseUrl) return '<not set>';
|
|
|
|
try {
|
|
const url = new URL(config.databaseUrl);
|
|
// Mask password
|
|
if (url.password) {
|
|
url.password = '***';
|
|
}
|
|
return url.toString();
|
|
} catch {
|
|
// If not a valid URL, return partially masked version
|
|
const url = config.databaseUrl;
|
|
if (url.includes(':')) {
|
|
const parts = url.split('@');
|
|
if (parts.length > 1) {
|
|
return `***@${parts[1]}`;
|
|
}
|
|
}
|
|
return url.substring(0, 20) + '...';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Print configuration on startup
|
|
*/
|
|
export function printConfig() {
|
|
const errors = validateConfig();
|
|
|
|
console.log(`
|
|
╔═══════════════════════════════════════════════════════╗
|
|
║ Memento MCP Server Configuration ║
|
|
╚═══════════════════════════════════════════════════════╝
|
|
|
|
Environment: ${config.nodeEnv.toUpperCase()}
|
|
Port: ${config.port}
|
|
Database: ${config.isPostgres ? 'PostgreSQL' : 'SQLite'}
|
|
Database URL: ${getSafeDatabaseUrl()}
|
|
|
|
Authentication: ${config.requireAuth ? 'ENABLED' : 'DISABLED'}
|
|
Static Key: ${config.staticApiKey ? 'SET' : 'NOT SET'}
|
|
Rate Limit: ${config.rateLimit} requests / ${config.rateLimitWindow}ms
|
|
|
|
Sessions:
|
|
Max: ${config.maxSessions}
|
|
TTL: ${config.sessionTtl}ms
|
|
Cleanup: ${config.sessionCleanupInterval}ms
|
|
|
|
Timeouts:
|
|
Request: ${config.requestTimeout}ms
|
|
DB Query: ${config.databaseQueryTimeout}ms
|
|
Tool: ${config.toolExecutionTimeout}ms
|
|
|
|
Cache: ${config.enableCache ? 'ENABLED' : 'DISABLED'}
|
|
TTL: ${config.cacheTtl}ms
|
|
Max Size: ${config.cacheMaxSize}
|
|
|
|
Features:
|
|
Metrics: ${config.enableMetrics ? 'ENABLED' : 'DISABLED'}
|
|
Audit Log: ${config.enableAuditLog ? 'ENABLED' : 'DISABLED'}
|
|
|
|
${errors.length > 0 ? `⚠️ CONFIGURATION WARNINGS/ERRORS:
|
|
${errors.map((e) => ` ${e.critical ? '❌' : '⚠️'} ${e.key}: ${e.message}`).join('\n')}
|
|
` : ''}
|
|
`);
|
|
}
|
|
|
|
export default config;
|