/** * Configuration Schema Validator Implementation * * Validates credential configuration files against defined schemas * to ensure data integrity and proper format before loading. * Supports JSON and YAML configuration formats. */ import { ConfigFile } from '@/types/credential' // ============================================================================ // Types and Interfaces // ============================================================================ export interface ValidationResult { valid: boolean errors: ValidationError[] warnings: ValidationWarning[] normalizedConfig?: ConfigFile } export interface ValidationError { path: string message: string code: ValidationErrorCode value?: any } export interface ValidationWarning { path: string message: string code: ValidationWarningCode value?: any } export enum ValidationErrorCode { MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD', INVALID_TYPE = 'INVALID_TYPE', INVALID_FORMAT = 'INVALID_FORMAT', INVALID_ENUM_VALUE = 'INVALID_ENUM_VALUE', DUPLICATE_ACCOUNT_ID = 'DUPLICATE_ACCOUNT_ID', INVALID_CREDENTIALS = 'INVALID_CREDENTIALS', UNSUPPORTED_VERSION = 'UNSUPPORTED_VERSION', INVALID_PLATFORM = 'INVALID_PLATFORM', } export enum ValidationWarningCode { DEPRECATED_FIELD = 'DEPRECATED_FIELD', MISSING_OPTIONAL_FIELD = 'MISSING_OPTIONAL_FIELD', UNUSUAL_VALUE = 'UNUSUAL_VALUE', PERFORMANCE_CONCERN = 'PERFORMANCE_CONCERN', } export interface ValidatorOptions { strictMode?: boolean allowUnknownFields?: boolean normalizeData?: boolean validateCredentials?: boolean maxAccounts?: number } // ============================================================================ // Schema Definitions // ============================================================================ const SUPPORTED_VERSIONS = ['1.0'] const PACIFICA_PRIVATE_KEY_PATTERN = /^[0-9a-fA-F]{64}$/ const ASTER_PRIVATE_KEY_PATTERN = /^0x[0-9a-fA-F]{64}$/ const ETHEREUM_ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/ const BINANCE_API_KEY_PATTERN = /^[A-Za-z0-9]{64}$/ // ============================================================================ // Configuration Validator Implementation // ============================================================================ export class ConfigValidator { private options: Required constructor(options: ValidatorOptions = {}) { this.options = { strictMode: options.strictMode ?? false, allowUnknownFields: options.allowUnknownFields ?? true, normalizeData: options.normalizeData ?? true, validateCredentials: options.validateCredentials ?? true, maxAccounts: options.maxAccounts ?? 1000, } } /** * Validate configuration file content */ validateConfig(config: any): ValidationResult { const errors: ValidationError[] = [] const warnings: ValidationWarning[] = [] let normalizedConfig: ConfigFile | undefined try { // Basic structure validation this.validateBasicStructure(config, errors) if (errors.length > 0 && this.options.strictMode) { return { valid: false, errors, warnings } } // Version validation this.validateVersion(config.version, errors, warnings) // Accounts validation if (config.accounts) { this.validateAccounts(config.accounts, errors, warnings) } // Normalize data if requested and no critical errors if (this.options.normalizeData && errors.length === 0) { normalizedConfig = this.normalizeConfig(config) } const valid = errors.length === 0 return { valid, errors, warnings, normalizedConfig, } } catch (error) { errors.push({ path: 'root', message: `Validation failed: ${error.message}`, code: ValidationErrorCode.INVALID_FORMAT, }) return { valid: false, errors, warnings } } } /** * Validate a single account configuration */ validateAccount(account: any, accountIndex?: number): ValidationResult { const errors: ValidationError[] = [] const warnings: ValidationWarning[] = [] const pathPrefix = accountIndex !== undefined ? `accounts[${accountIndex}]` : 'account' this.validateSingleAccount(account, pathPrefix, errors, warnings) return { valid: errors.length === 0, errors, warnings, } } /** * Validate credentials for a specific platform */ validateCredentials(platform: Platform, credentials: any): ValidationResult { const errors: ValidationError[] = [] const warnings: ValidationWarning[] = [] this.validateCredentialsByPlatform(platform, credentials, 'credentials', errors, warnings) return { valid: errors.length === 0, errors, warnings, } } /** * Quick validation check (returns boolean only) */ isValid(config: any): boolean { const result = this.validateConfig(config) return result.valid } // ============================================================================ // Private Validation Methods // ============================================================================ /** * Validate basic configuration structure */ private validateBasicStructure(config: any, errors: ValidationError[]): void { if (typeof config !== 'object' || config === null) { errors.push({ path: 'root', message: 'Configuration must be an object', code: ValidationErrorCode.INVALID_TYPE, value: typeof config, }) return } // Required fields if (!config.version) { errors.push({ path: 'version', message: 'Version field is required', code: ValidationErrorCode.MISSING_REQUIRED_FIELD, }) } if (!config.accounts) { errors.push({ path: 'accounts', message: 'Accounts field is required', code: ValidationErrorCode.MISSING_REQUIRED_FIELD, }) } else if (!Array.isArray(config.accounts)) { errors.push({ path: 'accounts', message: 'Accounts must be an array', code: ValidationErrorCode.INVALID_TYPE, value: typeof config.accounts, }) } } /** * Validate configuration version */ private validateVersion(version: any, errors: ValidationError[], warnings: ValidationWarning[]): void { if (typeof version !== 'string') { errors.push({ path: 'version', message: 'Version must be a string', code: ValidationErrorCode.INVALID_TYPE, value: typeof version, }) return } if (!SUPPORTED_VERSIONS.includes(version)) { errors.push({ path: 'version', message: `Unsupported version: ${version}. Supported versions: ${SUPPORTED_VERSIONS.join(', ')}`, code: ValidationErrorCode.UNSUPPORTED_VERSION, value: version, }) } } /** * Validate accounts array */ private validateAccounts(accounts: any[], errors: ValidationError[], warnings: ValidationWarning[]): void { if (accounts.length === 0) { warnings.push({ path: 'accounts', message: 'No accounts defined', code: ValidationWarningCode.UNUSUAL_VALUE, value: 0, }) return } if (accounts.length > this.options.maxAccounts) { errors.push({ path: 'accounts', message: `Too many accounts: ${accounts.length}. Maximum allowed: ${this.options.maxAccounts}`, code: ValidationErrorCode.INVALID_FORMAT, value: accounts.length, }) } // Check for duplicate account IDs const accountIds = new Set() const duplicates = new Set() accounts.forEach((account, index) => { if (account && account.id) { if (accountIds.has(account.id)) { duplicates.add(account.id) } accountIds.add(account.id) } this.validateSingleAccount(account, `accounts[${index}]`, errors, warnings) }) // Report duplicate IDs duplicates.forEach(id => { errors.push({ path: 'accounts', message: `Duplicate account ID: ${id}`, code: ValidationErrorCode.DUPLICATE_ACCOUNT_ID, value: id, }) }) // Performance warning for large number of accounts if (accounts.length > 100) { warnings.push({ path: 'accounts', message: `Large number of accounts (${accounts.length}) may impact performance`, code: ValidationWarningCode.PERFORMANCE_CONCERN, value: accounts.length, }) } } /** * Validate a single account */ private validateSingleAccount( account: any, pathPrefix: string, errors: ValidationError[], warnings: ValidationWarning[], ): void { if (typeof account !== 'object' || account === null) { errors.push({ path: pathPrefix, message: 'Account must be an object', code: ValidationErrorCode.INVALID_TYPE, value: typeof account, }) return } // Required fields if (!account.id) { errors.push({ path: `${pathPrefix}.id`, message: 'Account ID is required', code: ValidationErrorCode.MISSING_REQUIRED_FIELD, }) } else if (typeof account.id !== 'string') { errors.push({ path: `${pathPrefix}.id`, message: 'Account ID must be a string', code: ValidationErrorCode.INVALID_TYPE, value: typeof account.id, }) } else if (account.id.length === 0) { errors.push({ path: `${pathPrefix}.id`, message: 'Account ID cannot be empty', code: ValidationErrorCode.INVALID_FORMAT, value: account.id, }) } // Platform validation if (!account.platform) { errors.push({ path: `${pathPrefix}.platform`, message: 'Platform is required', code: ValidationErrorCode.MISSING_REQUIRED_FIELD, }) } else if (!Object.values(Platform).includes(account.platform)) { errors.push({ path: `${pathPrefix}.platform`, message: `Invalid platform: ${account.platform}. Valid platforms: ${Object.values(Platform).join(', ')}`, code: ValidationErrorCode.INVALID_PLATFORM, value: account.platform, }) } // Credentials validation if (!account.credentials) { errors.push({ path: `${pathPrefix}.credentials`, message: 'Credentials are required', code: ValidationErrorCode.MISSING_REQUIRED_FIELD, }) } else if (this.options.validateCredentials && account.platform) { this.validateCredentialsByPlatform( account.platform, account.credentials, `${pathPrefix}.credentials`, errors, warnings, ) } // Metadata validation (optional) if (account.metadata !== undefined) { if (typeof account.metadata !== 'object' || account.metadata === null) { errors.push({ path: `${pathPrefix}.metadata`, message: 'Metadata must be an object', code: ValidationErrorCode.INVALID_TYPE, value: typeof account.metadata, }) } } // Check for unknown fields in strict mode if (this.options.strictMode && !this.options.allowUnknownFields) { const allowedFields = ['id', 'platform', 'credentials', 'metadata'] const unknownFields = Object.keys(account).filter(key => !allowedFields.includes(key)) unknownFields.forEach(field => { errors.push({ path: `${pathPrefix}.${field}`, message: `Unknown field: ${field}`, code: ValidationErrorCode.INVALID_FORMAT, value: account[field], }) }) } } /** * Validate credentials based on platform */ private validateCredentialsByPlatform( platform: Platform, credentials: any, pathPrefix: string, errors: ValidationError[], warnings: ValidationWarning[], ): void { if (typeof credentials !== 'object' || credentials === null) { errors.push({ path: pathPrefix, message: 'Credentials must be an object', code: ValidationErrorCode.INVALID_TYPE, value: typeof credentials, }) return } switch (platform) { case Platform.PACIFICA: this.validatePacificaCredentials(credentials, pathPrefix, errors, warnings) break case Platform.ASTER: this.validateAsterCredentials(credentials, pathPrefix, errors, warnings) break case Platform.BINANCE: this.validateBinanceCredentials(credentials, pathPrefix, errors, warnings) break default: errors.push({ path: pathPrefix, message: `Unsupported platform for credential validation: ${platform}`, code: ValidationErrorCode.INVALID_PLATFORM, value: platform, }) } } /** * Validate Pacifica platform credentials */ private validatePacificaCredentials( credentials: any, pathPrefix: string, errors: ValidationError[], warnings: ValidationWarning[], ): void { if (credentials.type !== 'pacifica') { errors.push({ path: `${pathPrefix}.type`, message: 'Pacifica credentials must have type "pacifica"', code: ValidationErrorCode.INVALID_FORMAT, value: credentials.type, }) } if (!credentials.privateKey) { errors.push({ path: `${pathPrefix}.privateKey`, message: 'Pacifica private key is required', code: ValidationErrorCode.MISSING_REQUIRED_FIELD, }) } else if (typeof credentials.privateKey !== 'string') { errors.push({ path: `${pathPrefix}.privateKey`, message: 'Pacifica private key must be a string', code: ValidationErrorCode.INVALID_TYPE, value: typeof credentials.privateKey, }) } else if (!PACIFICA_PRIVATE_KEY_PATTERN.test(credentials.privateKey)) { errors.push({ path: `${pathPrefix}.privateKey`, message: 'Pacifica private key must be 64 hexadecimal characters', code: ValidationErrorCode.INVALID_FORMAT, value: `${credentials.privateKey.length} characters`, }) } } /** * Validate Aster platform credentials */ private validateAsterCredentials( credentials: any, pathPrefix: string, errors: ValidationError[], warnings: ValidationWarning[], ): void { if (credentials.type !== 'aster') { errors.push({ path: `${pathPrefix}.type`, message: 'Aster credentials must have type "aster"', code: ValidationErrorCode.INVALID_FORMAT, value: credentials.type, }) } if (!credentials.privateKey) { errors.push({ path: `${pathPrefix}.privateKey`, message: 'Aster private key is required', code: ValidationErrorCode.MISSING_REQUIRED_FIELD, }) } else if (typeof credentials.privateKey !== 'string') { errors.push({ path: `${pathPrefix}.privateKey`, message: 'Aster private key must be a string', code: ValidationErrorCode.INVALID_TYPE, value: typeof credentials.privateKey, }) } else if (!ASTER_PRIVATE_KEY_PATTERN.test(credentials.privateKey)) { errors.push({ path: `${pathPrefix}.privateKey`, message: 'Aster private key must be 66 characters starting with "0x" followed by 64 hexadecimal characters', code: ValidationErrorCode.INVALID_FORMAT, value: `${credentials.privateKey.length} characters`, }) } } /** * Validate Binance platform credentials */ private validateBinanceCredentials( credentials: any, pathPrefix: string, errors: ValidationError[], warnings: ValidationWarning[], ): void { if (credentials.type !== 'binance') { errors.push({ path: `${pathPrefix}.type`, message: 'Binance credentials must have type "binance"', code: ValidationErrorCode.INVALID_FORMAT, value: credentials.type, }) } if (!credentials.apiKey) { errors.push({ path: `${pathPrefix}.apiKey`, message: 'Binance API key is required', code: ValidationErrorCode.MISSING_REQUIRED_FIELD, }) } else if (typeof credentials.apiKey !== 'string') { errors.push({ path: `${pathPrefix}.apiKey`, message: 'Binance API key must be a string', code: ValidationErrorCode.INVALID_TYPE, value: typeof credentials.apiKey, }) } else if (!BINANCE_API_KEY_PATTERN.test(credentials.apiKey)) { errors.push({ path: `${pathPrefix}.apiKey`, message: 'Binance API key must be 64 alphanumeric characters', code: ValidationErrorCode.INVALID_FORMAT, value: `${credentials.apiKey.length} characters`, }) } if (!credentials.secretKey) { errors.push({ path: `${pathPrefix}.secretKey`, message: 'Binance secret key is required', code: ValidationErrorCode.MISSING_REQUIRED_FIELD, }) } else if (typeof credentials.secretKey !== 'string') { errors.push({ path: `${pathPrefix}.secretKey`, message: 'Binance secret key must be a string', code: ValidationErrorCode.INVALID_TYPE, value: typeof credentials.secretKey, }) } else if (credentials.secretKey.length === 0) { errors.push({ path: `${pathPrefix}.secretKey`, message: 'Binance secret key cannot be empty', code: ValidationErrorCode.INVALID_FORMAT, value: credentials.secretKey, }) } } /** * Normalize configuration data */ private normalizeConfig(config: any): ConfigFile { const normalized: ConfigFile = { version: config.version, accounts: [], } if (Array.isArray(config.accounts)) { normalized.accounts = config.accounts.map((account: any) => ({ id: account.id, platform: account.platform, credentials: account.credentials, metadata: account.metadata || {}, })) } return normalized } } // ============================================================================ // Utility Functions // ============================================================================ /** * Create a validator with default options */ export function createValidator(options?: ValidatorOptions): ConfigValidator { return new ConfigValidator(options) } /** * Create a strict validator for production use */ export function createStrictValidator(): ConfigValidator { return new ConfigValidator({ strictMode: true, allowUnknownFields: false, normalizeData: true, validateCredentials: true, maxAccounts: 500, }) } /** * Create a lenient validator for development */ export function createLenientValidator(): ConfigValidator { return new ConfigValidator({ strictMode: false, allowUnknownFields: true, normalizeData: true, validateCredentials: false, maxAccounts: 1000, }) } /** * Quick validation function */ export function validateConfig(config: any, options?: ValidatorOptions): ValidationResult { const validator = new ConfigValidator(options) return validator.validateConfig(config) } /** * Quick validation check (boolean only) */ export function isValidConfig(config: any, options?: ValidatorOptions): boolean { const validator = new ConfigValidator(options) return validator.isValid(config) } // ============================================================================ // Export // ============================================================================ export default ConfigValidator