hook-engine
Version:
Production-grade webhook engine with comprehensive adapter support, security, reliability, structured logging, and CLI tools.
370 lines (369 loc) ⢠13.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateCommand = validateCommand;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const fs_1 = require("fs");
async function validateCommand(configPath, options) {
console.log('š Validating Hook Engine configuration...\n');
try {
// Check if config file exists
if (!(0, fs_1.existsSync)(configPath)) {
console.error(`ā Configuration file not found: ${configPath}`);
process.exit(1);
}
console.log(`š Config file: ${path_1.default.resolve(configPath)}`);
console.log(`š§ Validation mode: ${options.strict ? 'strict' : 'standard'}\n`);
// Load and parse configuration
const config = await loadConfiguration(configPath);
// Validate configuration
const result = await validateConfiguration(config, options);
// Output results
await outputResults(result, options);
// Exit with appropriate code
if (!result.valid) {
process.exit(1);
}
console.log('\nā
Configuration validation completed successfully!');
}
catch (error) {
console.error('ā Validation failed:', error.message);
process.exit(1);
}
}
async function loadConfiguration(configPath) {
const content = await promises_1.default.readFile(configPath, 'utf-8');
const ext = path_1.default.extname(configPath).toLowerCase();
try {
switch (ext) {
case '.json':
return JSON.parse(content);
case '.js':
case '.ts':
// For JS/TS files, we need to evaluate them
// In a real implementation, you'd use a proper module loader
const moduleContent = content.replace(/export\s+default\s+/, 'module.exports = ');
const tempFile = path_1.default.join(process.cwd(), '.temp-config.js');
await promises_1.default.writeFile(tempFile, moduleContent);
const config = require(tempFile);
await promises_1.default.unlink(tempFile);
return config;
case '.yaml':
case '.yml':
// Would use yaml parser in real implementation
throw new Error('YAML configuration files not yet supported');
default:
throw new Error(`Unsupported configuration file format: ${ext}`);
}
}
catch (error) {
throw new Error(`Failed to parse configuration file: ${error.message}`);
}
}
async function validateConfiguration(config, options) {
const errors = [];
const warnings = [];
let totalChecks = 0;
// Validate required fields
totalChecks++;
if (!config.adapters) {
errors.push({
path: 'adapters',
message: 'Adapters configuration is required',
severity: 'error',
code: 'MISSING_ADAPTERS'
});
}
// Validate adapters
if (config.adapters) {
const adapterResults = validateAdapters(config.adapters, options);
errors.push(...adapterResults.errors);
warnings.push(...adapterResults.warnings);
totalChecks += adapterResults.totalChecks;
}
// Validate storage configuration
if (config.storage) {
const storageResults = validateStorage(config.storage, options);
errors.push(...storageResults.errors);
warnings.push(...storageResults.warnings);
totalChecks += storageResults.totalChecks;
}
else if (options.strict) {
errors.push({
path: 'storage',
message: 'Storage configuration is required in strict mode',
severity: 'error',
code: 'MISSING_STORAGE'
});
}
// Validate retry configuration
if (config.retry) {
const retryResults = validateRetry(config.retry, options);
errors.push(...retryResults.errors);
warnings.push(...retryResults.warnings);
totalChecks += retryResults.totalChecks;
}
// Validate security configuration
if (config.security) {
const securityResults = validateSecurity(config.security, options);
errors.push(...securityResults.errors);
warnings.push(...securityResults.warnings);
totalChecks += securityResults.totalChecks;
}
// Validate observability configuration
if (config.observability) {
const observabilityResults = validateObservability(config.observability, options);
errors.push(...observabilityResults.errors);
warnings.push(...observabilityResults.warnings);
totalChecks += observabilityResults.totalChecks;
}
const failed = errors.filter(e => e.severity === 'error').length;
const passed = totalChecks - failed;
return {
valid: failed === 0,
errors,
warnings,
summary: {
totalChecks,
passed,
failed,
warnings: warnings.length
}
};
}
function validateAdapters(adapters, options) {
const errors = [];
const warnings = [];
let totalChecks = 0;
const supportedAdapters = ['github', 'stripe', 'shopify', 'discord', 'twilio', 'sendgrid', 'paypal'];
for (const [adapterName, adapterConfig] of Object.entries(adapters)) {
totalChecks++;
if (!supportedAdapters.includes(adapterName)) {
warnings.push({
path: `adapters.${adapterName}`,
message: `Unknown adapter: ${adapterName}`,
suggestion: `Supported adapters: ${supportedAdapters.join(', ')}`
});
}
// Validate adapter-specific configuration
if (adapterName === 'github') {
totalChecks++;
if (!adapterConfig.secret) {
errors.push({
path: `adapters.${adapterName}.secret`,
message: 'GitHub webhook secret is required',
severity: 'error',
code: 'MISSING_SECRET'
});
}
}
if (adapterName === 'stripe') {
totalChecks++;
if (!adapterConfig.secret) {
errors.push({
path: `adapters.${adapterName}.secret`,
message: 'Stripe webhook secret is required',
severity: 'error',
code: 'MISSING_SECRET'
});
}
}
}
return { errors, warnings, totalChecks };
}
function validateStorage(storage, options) {
const errors = [];
const warnings = [];
let totalChecks = 1;
const supportedTypes = ['sqlite', 'postgresql', 'mysql', 'memory'];
if (!storage.type) {
errors.push({
path: 'storage.type',
message: 'Storage type is required',
severity: 'error',
code: 'MISSING_STORAGE_TYPE'
});
}
else if (!supportedTypes.includes(storage.type)) {
errors.push({
path: 'storage.type',
message: `Unsupported storage type: ${storage.type}`,
severity: 'error',
code: 'INVALID_STORAGE_TYPE'
});
}
// Validate storage-specific configuration
if (storage.type === 'sqlite') {
totalChecks++;
if (!storage.config?.database) {
errors.push({
path: 'storage.config.database',
message: 'SQLite database path is required',
severity: 'error',
code: 'MISSING_DATABASE_PATH'
});
}
}
if (storage.type === 'postgresql' || storage.type === 'mysql') {
totalChecks += 3;
if (!storage.config?.host) {
errors.push({
path: 'storage.config.host',
message: 'Database host is required',
severity: 'error',
code: 'MISSING_DB_HOST'
});
}
if (!storage.config?.database) {
errors.push({
path: 'storage.config.database',
message: 'Database name is required',
severity: 'error',
code: 'MISSING_DB_NAME'
});
}
if (!storage.config?.user) {
warnings.push({
path: 'storage.config.user',
message: 'Database user not specified',
suggestion: 'Consider specifying a database user for security'
});
}
}
return { errors, warnings, totalChecks };
}
function validateRetry(retry, options) {
const errors = [];
const warnings = [];
let totalChecks = 3;
if (typeof retry.maxAttempts !== 'number' || retry.maxAttempts < 1) {
errors.push({
path: 'retry.maxAttempts',
message: 'maxAttempts must be a positive number',
severity: 'error',
code: 'INVALID_MAX_ATTEMPTS'
});
}
if (typeof retry.baseDelay !== 'number' || retry.baseDelay < 0) {
errors.push({
path: 'retry.baseDelay',
message: 'baseDelay must be a non-negative number',
severity: 'error',
code: 'INVALID_BASE_DELAY'
});
}
if (retry.maxDelay && (typeof retry.maxDelay !== 'number' || retry.maxDelay < retry.baseDelay)) {
errors.push({
path: 'retry.maxDelay',
message: 'maxDelay must be greater than or equal to baseDelay',
severity: 'error',
code: 'INVALID_MAX_DELAY'
});
}
if (retry.maxAttempts > 10) {
warnings.push({
path: 'retry.maxAttempts',
message: 'High number of retry attempts may impact performance',
suggestion: 'Consider reducing maxAttempts to 5 or less'
});
}
return { errors, warnings, totalChecks };
}
function validateSecurity(security, options) {
const errors = [];
const warnings = [];
let totalChecks = 0;
if (security.rateLimiting) {
totalChecks += 2;
if (typeof security.rateLimiting.maxRequests !== 'number' || security.rateLimiting.maxRequests < 1) {
errors.push({
path: 'security.rateLimiting.maxRequests',
message: 'maxRequests must be a positive number',
severity: 'error',
code: 'INVALID_RATE_LIMIT'
});
}
if (typeof security.rateLimiting.windowMs !== 'number' || security.rateLimiting.windowMs < 1000) {
errors.push({
path: 'security.rateLimiting.windowMs',
message: 'windowMs must be at least 1000ms',
severity: 'error',
code: 'INVALID_RATE_WINDOW'
});
}
}
if (security.ipAllowlist?.enabled && (!security.ipAllowlist.allowedIPs || security.ipAllowlist.allowedIPs.length === 0)) {
warnings.push({
path: 'security.ipAllowlist.allowedIPs',
message: 'IP allowlist is enabled but no IPs are specified',
suggestion: 'Add allowed IP addresses or disable IP allowlisting'
});
}
return { errors, warnings, totalChecks };
}
function validateObservability(observability, options) {
const errors = [];
const warnings = [];
let totalChecks = 0;
if (observability.logging) {
totalChecks++;
const validLevels = ['error', 'warn', 'info', 'debug'];
if (observability.logging.level && !validLevels.includes(observability.logging.level)) {
errors.push({
path: 'observability.logging.level',
message: `Invalid log level: ${observability.logging.level}`,
severity: 'error',
code: 'INVALID_LOG_LEVEL'
});
}
}
return { errors, warnings, totalChecks };
}
async function outputResults(result, options) {
switch (options.format) {
case 'json':
console.log(JSON.stringify(result, null, 2));
break;
case 'yaml':
// Would use yaml serializer in real implementation
console.log('YAML output not yet implemented');
break;
case 'table':
default:
outputTableFormat(result);
break;
}
}
function outputTableFormat(result) {
console.log('š Validation Results:');
console.log('ā'.repeat(50));
console.log(`Total Checks: ${result.summary.totalChecks}`);
console.log(`ā
Passed: ${result.summary.passed}`);
console.log(`ā Failed: ${result.summary.failed}`);
console.log(`ā ļø Warnings: ${result.summary.warnings}`);
console.log('ā'.repeat(50));
if (result.errors.length > 0) {
console.log('\nā Errors:');
result.errors.forEach((error, index) => {
console.log(`${index + 1}. ${error.path}: ${error.message} (${error.code})`);
});
}
if (result.warnings.length > 0) {
console.log('\nā ļø Warnings:');
result.warnings.forEach((warning, index) => {
console.log(`${index + 1}. ${warning.path}: ${warning.message}`);
if (warning.suggestion) {
console.log(` š” Suggestion: ${warning.suggestion}`);
}
});
}
if (result.valid) {
console.log('\nš Configuration is valid!');
}
else {
console.log('\nš„ Configuration has errors that need to be fixed.');
}
}