ConfigValidator.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. /**
  2. * Configuration Schema Validator Implementation
  3. *
  4. * Validates credential configuration files against defined schemas
  5. * to ensure data integrity and proper format before loading.
  6. * Supports JSON and YAML configuration formats.
  7. */
  8. import { ConfigFile } from '@/types/credential'
  9. // ============================================================================
  10. // Types and Interfaces
  11. // ============================================================================
  12. export interface ValidationResult {
  13. valid: boolean
  14. errors: ValidationError[]
  15. warnings: ValidationWarning[]
  16. normalizedConfig?: ConfigFile
  17. }
  18. export interface ValidationError {
  19. path: string
  20. message: string
  21. code: ValidationErrorCode
  22. value?: any
  23. }
  24. export interface ValidationWarning {
  25. path: string
  26. message: string
  27. code: ValidationWarningCode
  28. value?: any
  29. }
  30. export enum ValidationErrorCode {
  31. MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD',
  32. INVALID_TYPE = 'INVALID_TYPE',
  33. INVALID_FORMAT = 'INVALID_FORMAT',
  34. INVALID_ENUM_VALUE = 'INVALID_ENUM_VALUE',
  35. DUPLICATE_ACCOUNT_ID = 'DUPLICATE_ACCOUNT_ID',
  36. INVALID_CREDENTIALS = 'INVALID_CREDENTIALS',
  37. UNSUPPORTED_VERSION = 'UNSUPPORTED_VERSION',
  38. INVALID_PLATFORM = 'INVALID_PLATFORM',
  39. }
  40. export enum ValidationWarningCode {
  41. DEPRECATED_FIELD = 'DEPRECATED_FIELD',
  42. MISSING_OPTIONAL_FIELD = 'MISSING_OPTIONAL_FIELD',
  43. UNUSUAL_VALUE = 'UNUSUAL_VALUE',
  44. PERFORMANCE_CONCERN = 'PERFORMANCE_CONCERN',
  45. }
  46. export interface ValidatorOptions {
  47. strictMode?: boolean
  48. allowUnknownFields?: boolean
  49. normalizeData?: boolean
  50. validateCredentials?: boolean
  51. maxAccounts?: number
  52. }
  53. // ============================================================================
  54. // Schema Definitions
  55. // ============================================================================
  56. const SUPPORTED_VERSIONS = ['1.0']
  57. const PACIFICA_PRIVATE_KEY_PATTERN = /^[0-9a-fA-F]{64}$/
  58. const ASTER_PRIVATE_KEY_PATTERN = /^0x[0-9a-fA-F]{64}$/
  59. const ETHEREUM_ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/
  60. const BINANCE_API_KEY_PATTERN = /^[A-Za-z0-9]{64}$/
  61. // ============================================================================
  62. // Configuration Validator Implementation
  63. // ============================================================================
  64. export class ConfigValidator {
  65. private options: Required<ValidatorOptions>
  66. constructor(options: ValidatorOptions = {}) {
  67. this.options = {
  68. strictMode: options.strictMode ?? false,
  69. allowUnknownFields: options.allowUnknownFields ?? true,
  70. normalizeData: options.normalizeData ?? true,
  71. validateCredentials: options.validateCredentials ?? true,
  72. maxAccounts: options.maxAccounts ?? 1000,
  73. }
  74. }
  75. /**
  76. * Validate configuration file content
  77. */
  78. validateConfig(config: any): ValidationResult {
  79. const errors: ValidationError[] = []
  80. const warnings: ValidationWarning[] = []
  81. let normalizedConfig: ConfigFile | undefined
  82. try {
  83. // Basic structure validation
  84. this.validateBasicStructure(config, errors)
  85. if (errors.length > 0 && this.options.strictMode) {
  86. return { valid: false, errors, warnings }
  87. }
  88. // Version validation
  89. this.validateVersion(config.version, errors, warnings)
  90. // Accounts validation
  91. if (config.accounts) {
  92. this.validateAccounts(config.accounts, errors, warnings)
  93. }
  94. // Normalize data if requested and no critical errors
  95. if (this.options.normalizeData && errors.length === 0) {
  96. normalizedConfig = this.normalizeConfig(config)
  97. }
  98. const valid = errors.length === 0
  99. return {
  100. valid,
  101. errors,
  102. warnings,
  103. normalizedConfig,
  104. }
  105. } catch (error) {
  106. errors.push({
  107. path: 'root',
  108. message: `Validation failed: ${error.message}`,
  109. code: ValidationErrorCode.INVALID_FORMAT,
  110. })
  111. return { valid: false, errors, warnings }
  112. }
  113. }
  114. /**
  115. * Validate a single account configuration
  116. */
  117. validateAccount(account: any, accountIndex?: number): ValidationResult {
  118. const errors: ValidationError[] = []
  119. const warnings: ValidationWarning[] = []
  120. const pathPrefix = accountIndex !== undefined ? `accounts[${accountIndex}]` : 'account'
  121. this.validateSingleAccount(account, pathPrefix, errors, warnings)
  122. return {
  123. valid: errors.length === 0,
  124. errors,
  125. warnings,
  126. }
  127. }
  128. /**
  129. * Validate credentials for a specific platform
  130. */
  131. validateCredentials(platform: Platform, credentials: any): ValidationResult {
  132. const errors: ValidationError[] = []
  133. const warnings: ValidationWarning[] = []
  134. this.validateCredentialsByPlatform(platform, credentials, 'credentials', errors, warnings)
  135. return {
  136. valid: errors.length === 0,
  137. errors,
  138. warnings,
  139. }
  140. }
  141. /**
  142. * Quick validation check (returns boolean only)
  143. */
  144. isValid(config: any): boolean {
  145. const result = this.validateConfig(config)
  146. return result.valid
  147. }
  148. // ============================================================================
  149. // Private Validation Methods
  150. // ============================================================================
  151. /**
  152. * Validate basic configuration structure
  153. */
  154. private validateBasicStructure(config: any, errors: ValidationError[]): void {
  155. if (typeof config !== 'object' || config === null) {
  156. errors.push({
  157. path: 'root',
  158. message: 'Configuration must be an object',
  159. code: ValidationErrorCode.INVALID_TYPE,
  160. value: typeof config,
  161. })
  162. return
  163. }
  164. // Required fields
  165. if (!config.version) {
  166. errors.push({
  167. path: 'version',
  168. message: 'Version field is required',
  169. code: ValidationErrorCode.MISSING_REQUIRED_FIELD,
  170. })
  171. }
  172. if (!config.accounts) {
  173. errors.push({
  174. path: 'accounts',
  175. message: 'Accounts field is required',
  176. code: ValidationErrorCode.MISSING_REQUIRED_FIELD,
  177. })
  178. } else if (!Array.isArray(config.accounts)) {
  179. errors.push({
  180. path: 'accounts',
  181. message: 'Accounts must be an array',
  182. code: ValidationErrorCode.INVALID_TYPE,
  183. value: typeof config.accounts,
  184. })
  185. }
  186. }
  187. /**
  188. * Validate configuration version
  189. */
  190. private validateVersion(version: any, errors: ValidationError[], warnings: ValidationWarning[]): void {
  191. if (typeof version !== 'string') {
  192. errors.push({
  193. path: 'version',
  194. message: 'Version must be a string',
  195. code: ValidationErrorCode.INVALID_TYPE,
  196. value: typeof version,
  197. })
  198. return
  199. }
  200. if (!SUPPORTED_VERSIONS.includes(version)) {
  201. errors.push({
  202. path: 'version',
  203. message: `Unsupported version: ${version}. Supported versions: ${SUPPORTED_VERSIONS.join(', ')}`,
  204. code: ValidationErrorCode.UNSUPPORTED_VERSION,
  205. value: version,
  206. })
  207. }
  208. }
  209. /**
  210. * Validate accounts array
  211. */
  212. private validateAccounts(accounts: any[], errors: ValidationError[], warnings: ValidationWarning[]): void {
  213. if (accounts.length === 0) {
  214. warnings.push({
  215. path: 'accounts',
  216. message: 'No accounts defined',
  217. code: ValidationWarningCode.UNUSUAL_VALUE,
  218. value: 0,
  219. })
  220. return
  221. }
  222. if (accounts.length > this.options.maxAccounts) {
  223. errors.push({
  224. path: 'accounts',
  225. message: `Too many accounts: ${accounts.length}. Maximum allowed: ${this.options.maxAccounts}`,
  226. code: ValidationErrorCode.INVALID_FORMAT,
  227. value: accounts.length,
  228. })
  229. }
  230. // Check for duplicate account IDs
  231. const accountIds = new Set<string>()
  232. const duplicates = new Set<string>()
  233. accounts.forEach((account, index) => {
  234. if (account && account.id) {
  235. if (accountIds.has(account.id)) {
  236. duplicates.add(account.id)
  237. }
  238. accountIds.add(account.id)
  239. }
  240. this.validateSingleAccount(account, `accounts[${index}]`, errors, warnings)
  241. })
  242. // Report duplicate IDs
  243. duplicates.forEach(id => {
  244. errors.push({
  245. path: 'accounts',
  246. message: `Duplicate account ID: ${id}`,
  247. code: ValidationErrorCode.DUPLICATE_ACCOUNT_ID,
  248. value: id,
  249. })
  250. })
  251. // Performance warning for large number of accounts
  252. if (accounts.length > 100) {
  253. warnings.push({
  254. path: 'accounts',
  255. message: `Large number of accounts (${accounts.length}) may impact performance`,
  256. code: ValidationWarningCode.PERFORMANCE_CONCERN,
  257. value: accounts.length,
  258. })
  259. }
  260. }
  261. /**
  262. * Validate a single account
  263. */
  264. private validateSingleAccount(
  265. account: any,
  266. pathPrefix: string,
  267. errors: ValidationError[],
  268. warnings: ValidationWarning[],
  269. ): void {
  270. if (typeof account !== 'object' || account === null) {
  271. errors.push({
  272. path: pathPrefix,
  273. message: 'Account must be an object',
  274. code: ValidationErrorCode.INVALID_TYPE,
  275. value: typeof account,
  276. })
  277. return
  278. }
  279. // Required fields
  280. if (!account.id) {
  281. errors.push({
  282. path: `${pathPrefix}.id`,
  283. message: 'Account ID is required',
  284. code: ValidationErrorCode.MISSING_REQUIRED_FIELD,
  285. })
  286. } else if (typeof account.id !== 'string') {
  287. errors.push({
  288. path: `${pathPrefix}.id`,
  289. message: 'Account ID must be a string',
  290. code: ValidationErrorCode.INVALID_TYPE,
  291. value: typeof account.id,
  292. })
  293. } else if (account.id.length === 0) {
  294. errors.push({
  295. path: `${pathPrefix}.id`,
  296. message: 'Account ID cannot be empty',
  297. code: ValidationErrorCode.INVALID_FORMAT,
  298. value: account.id,
  299. })
  300. }
  301. // Platform validation
  302. if (!account.platform) {
  303. errors.push({
  304. path: `${pathPrefix}.platform`,
  305. message: 'Platform is required',
  306. code: ValidationErrorCode.MISSING_REQUIRED_FIELD,
  307. })
  308. } else if (!Object.values(Platform).includes(account.platform)) {
  309. errors.push({
  310. path: `${pathPrefix}.platform`,
  311. message: `Invalid platform: ${account.platform}. Valid platforms: ${Object.values(Platform).join(', ')}`,
  312. code: ValidationErrorCode.INVALID_PLATFORM,
  313. value: account.platform,
  314. })
  315. }
  316. // Credentials validation
  317. if (!account.credentials) {
  318. errors.push({
  319. path: `${pathPrefix}.credentials`,
  320. message: 'Credentials are required',
  321. code: ValidationErrorCode.MISSING_REQUIRED_FIELD,
  322. })
  323. } else if (this.options.validateCredentials && account.platform) {
  324. this.validateCredentialsByPlatform(
  325. account.platform,
  326. account.credentials,
  327. `${pathPrefix}.credentials`,
  328. errors,
  329. warnings,
  330. )
  331. }
  332. // Metadata validation (optional)
  333. if (account.metadata !== undefined) {
  334. if (typeof account.metadata !== 'object' || account.metadata === null) {
  335. errors.push({
  336. path: `${pathPrefix}.metadata`,
  337. message: 'Metadata must be an object',
  338. code: ValidationErrorCode.INVALID_TYPE,
  339. value: typeof account.metadata,
  340. })
  341. }
  342. }
  343. // Check for unknown fields in strict mode
  344. if (this.options.strictMode && !this.options.allowUnknownFields) {
  345. const allowedFields = ['id', 'platform', 'credentials', 'metadata']
  346. const unknownFields = Object.keys(account).filter(key => !allowedFields.includes(key))
  347. unknownFields.forEach(field => {
  348. errors.push({
  349. path: `${pathPrefix}.${field}`,
  350. message: `Unknown field: ${field}`,
  351. code: ValidationErrorCode.INVALID_FORMAT,
  352. value: account[field],
  353. })
  354. })
  355. }
  356. }
  357. /**
  358. * Validate credentials based on platform
  359. */
  360. private validateCredentialsByPlatform(
  361. platform: Platform,
  362. credentials: any,
  363. pathPrefix: string,
  364. errors: ValidationError[],
  365. warnings: ValidationWarning[],
  366. ): void {
  367. if (typeof credentials !== 'object' || credentials === null) {
  368. errors.push({
  369. path: pathPrefix,
  370. message: 'Credentials must be an object',
  371. code: ValidationErrorCode.INVALID_TYPE,
  372. value: typeof credentials,
  373. })
  374. return
  375. }
  376. switch (platform) {
  377. case Platform.PACIFICA:
  378. this.validatePacificaCredentials(credentials, pathPrefix, errors, warnings)
  379. break
  380. case Platform.ASTER:
  381. this.validateAsterCredentials(credentials, pathPrefix, errors, warnings)
  382. break
  383. case Platform.BINANCE:
  384. this.validateBinanceCredentials(credentials, pathPrefix, errors, warnings)
  385. break
  386. default:
  387. errors.push({
  388. path: pathPrefix,
  389. message: `Unsupported platform for credential validation: ${platform}`,
  390. code: ValidationErrorCode.INVALID_PLATFORM,
  391. value: platform,
  392. })
  393. }
  394. }
  395. /**
  396. * Validate Pacifica platform credentials
  397. */
  398. private validatePacificaCredentials(
  399. credentials: any,
  400. pathPrefix: string,
  401. errors: ValidationError[],
  402. warnings: ValidationWarning[],
  403. ): void {
  404. if (credentials.type !== 'pacifica') {
  405. errors.push({
  406. path: `${pathPrefix}.type`,
  407. message: 'Pacifica credentials must have type "pacifica"',
  408. code: ValidationErrorCode.INVALID_FORMAT,
  409. value: credentials.type,
  410. })
  411. }
  412. if (!credentials.privateKey) {
  413. errors.push({
  414. path: `${pathPrefix}.privateKey`,
  415. message: 'Pacifica private key is required',
  416. code: ValidationErrorCode.MISSING_REQUIRED_FIELD,
  417. })
  418. } else if (typeof credentials.privateKey !== 'string') {
  419. errors.push({
  420. path: `${pathPrefix}.privateKey`,
  421. message: 'Pacifica private key must be a string',
  422. code: ValidationErrorCode.INVALID_TYPE,
  423. value: typeof credentials.privateKey,
  424. })
  425. } else if (!PACIFICA_PRIVATE_KEY_PATTERN.test(credentials.privateKey)) {
  426. errors.push({
  427. path: `${pathPrefix}.privateKey`,
  428. message: 'Pacifica private key must be 64 hexadecimal characters',
  429. code: ValidationErrorCode.INVALID_FORMAT,
  430. value: `${credentials.privateKey.length} characters`,
  431. })
  432. }
  433. }
  434. /**
  435. * Validate Aster platform credentials
  436. */
  437. private validateAsterCredentials(
  438. credentials: any,
  439. pathPrefix: string,
  440. errors: ValidationError[],
  441. warnings: ValidationWarning[],
  442. ): void {
  443. if (credentials.type !== 'aster') {
  444. errors.push({
  445. path: `${pathPrefix}.type`,
  446. message: 'Aster credentials must have type "aster"',
  447. code: ValidationErrorCode.INVALID_FORMAT,
  448. value: credentials.type,
  449. })
  450. }
  451. if (!credentials.privateKey) {
  452. errors.push({
  453. path: `${pathPrefix}.privateKey`,
  454. message: 'Aster private key is required',
  455. code: ValidationErrorCode.MISSING_REQUIRED_FIELD,
  456. })
  457. } else if (typeof credentials.privateKey !== 'string') {
  458. errors.push({
  459. path: `${pathPrefix}.privateKey`,
  460. message: 'Aster private key must be a string',
  461. code: ValidationErrorCode.INVALID_TYPE,
  462. value: typeof credentials.privateKey,
  463. })
  464. } else if (!ASTER_PRIVATE_KEY_PATTERN.test(credentials.privateKey)) {
  465. errors.push({
  466. path: `${pathPrefix}.privateKey`,
  467. message: 'Aster private key must be 66 characters starting with "0x" followed by 64 hexadecimal characters',
  468. code: ValidationErrorCode.INVALID_FORMAT,
  469. value: `${credentials.privateKey.length} characters`,
  470. })
  471. }
  472. }
  473. /**
  474. * Validate Binance platform credentials
  475. */
  476. private validateBinanceCredentials(
  477. credentials: any,
  478. pathPrefix: string,
  479. errors: ValidationError[],
  480. warnings: ValidationWarning[],
  481. ): void {
  482. if (credentials.type !== 'binance') {
  483. errors.push({
  484. path: `${pathPrefix}.type`,
  485. message: 'Binance credentials must have type "binance"',
  486. code: ValidationErrorCode.INVALID_FORMAT,
  487. value: credentials.type,
  488. })
  489. }
  490. if (!credentials.apiKey) {
  491. errors.push({
  492. path: `${pathPrefix}.apiKey`,
  493. message: 'Binance API key is required',
  494. code: ValidationErrorCode.MISSING_REQUIRED_FIELD,
  495. })
  496. } else if (typeof credentials.apiKey !== 'string') {
  497. errors.push({
  498. path: `${pathPrefix}.apiKey`,
  499. message: 'Binance API key must be a string',
  500. code: ValidationErrorCode.INVALID_TYPE,
  501. value: typeof credentials.apiKey,
  502. })
  503. } else if (!BINANCE_API_KEY_PATTERN.test(credentials.apiKey)) {
  504. errors.push({
  505. path: `${pathPrefix}.apiKey`,
  506. message: 'Binance API key must be 64 alphanumeric characters',
  507. code: ValidationErrorCode.INVALID_FORMAT,
  508. value: `${credentials.apiKey.length} characters`,
  509. })
  510. }
  511. if (!credentials.secretKey) {
  512. errors.push({
  513. path: `${pathPrefix}.secretKey`,
  514. message: 'Binance secret key is required',
  515. code: ValidationErrorCode.MISSING_REQUIRED_FIELD,
  516. })
  517. } else if (typeof credentials.secretKey !== 'string') {
  518. errors.push({
  519. path: `${pathPrefix}.secretKey`,
  520. message: 'Binance secret key must be a string',
  521. code: ValidationErrorCode.INVALID_TYPE,
  522. value: typeof credentials.secretKey,
  523. })
  524. } else if (credentials.secretKey.length === 0) {
  525. errors.push({
  526. path: `${pathPrefix}.secretKey`,
  527. message: 'Binance secret key cannot be empty',
  528. code: ValidationErrorCode.INVALID_FORMAT,
  529. value: credentials.secretKey,
  530. })
  531. }
  532. }
  533. /**
  534. * Normalize configuration data
  535. */
  536. private normalizeConfig(config: any): ConfigFile {
  537. const normalized: ConfigFile = {
  538. version: config.version,
  539. accounts: [],
  540. }
  541. if (Array.isArray(config.accounts)) {
  542. normalized.accounts = config.accounts.map((account: any) => ({
  543. id: account.id,
  544. platform: account.platform,
  545. credentials: account.credentials,
  546. metadata: account.metadata || {},
  547. }))
  548. }
  549. return normalized
  550. }
  551. }
  552. // ============================================================================
  553. // Utility Functions
  554. // ============================================================================
  555. /**
  556. * Create a validator with default options
  557. */
  558. export function createValidator(options?: ValidatorOptions): ConfigValidator {
  559. return new ConfigValidator(options)
  560. }
  561. /**
  562. * Create a strict validator for production use
  563. */
  564. export function createStrictValidator(): ConfigValidator {
  565. return new ConfigValidator({
  566. strictMode: true,
  567. allowUnknownFields: false,
  568. normalizeData: true,
  569. validateCredentials: true,
  570. maxAccounts: 500,
  571. })
  572. }
  573. /**
  574. * Create a lenient validator for development
  575. */
  576. export function createLenientValidator(): ConfigValidator {
  577. return new ConfigValidator({
  578. strictMode: false,
  579. allowUnknownFields: true,
  580. normalizeData: true,
  581. validateCredentials: false,
  582. maxAccounts: 1000,
  583. })
  584. }
  585. /**
  586. * Quick validation function
  587. */
  588. export function validateConfig(config: any, options?: ValidatorOptions): ValidationResult {
  589. const validator = new ConfigValidator(options)
  590. return validator.validateConfig(config)
  591. }
  592. /**
  593. * Quick validation check (boolean only)
  594. */
  595. export function isValidConfig(config: any, options?: ValidatorOptions): boolean {
  596. const validator = new ConfigValidator(options)
  597. return validator.isValid(config)
  598. }
  599. // ============================================================================
  600. // Export
  601. // ============================================================================
  602. export default ConfigValidator