CredentialManager.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. /**
  2. * Main Credential Manager Implementation
  3. *
  4. * Central orchestrator for multi-platform credential management.
  5. * Provides unified interface for configuration loading, account management,
  6. * and signing operations across different platforms.
  7. */
  8. import {
  9. ICredentialManager,
  10. LoadResult,
  11. Account,
  12. SignResult,
  13. Platform,
  14. CredentialErrorCode,
  15. CredentialManagerError,
  16. PerformanceMetrics,
  17. } from '@/types/credential'
  18. import { ConfigLoader } from './ConfigLoader'
  19. import { CredentialValidator } from './CredentialValidator'
  20. import { PlatformDetector } from './PlatformDetector'
  21. import { PacificaSigner } from './signers/PacificaSigner'
  22. /**
  23. * Main credential manager class
  24. */
  25. export class CredentialManager implements ICredentialManager {
  26. private configLoader = new ConfigLoader()
  27. private validator = new CredentialValidator()
  28. private platformDetector = new PlatformDetector()
  29. private accounts = new Map<string, Account>()
  30. private signers = new Map<Platform, any>()
  31. private isWatching = false
  32. private currentConfigPath?: string
  33. private metrics: PerformanceMetrics
  34. constructor() {
  35. // Initialize performance metrics
  36. this.metrics = {
  37. configLoadTime: 0,
  38. configWatchLatency: 0,
  39. signOperationsTotal: 0,
  40. signOperationsSuccess: 0,
  41. signOperationsFailed: 0,
  42. averageSignTime: 0,
  43. maxSignTime: 0,
  44. minSignTime: Infinity,
  45. verifyOperationsTotal: 0,
  46. verifyOperationsSuccess: 0,
  47. verifyOperationsFailed: 0,
  48. averageVerifyTime: 0,
  49. memoryUsage: 0,
  50. accountCount: 0,
  51. errorsByCode: Object.fromEntries(Object.values(CredentialErrorCode).map(code => [code, 0])) as Record<
  52. CredentialErrorCode,
  53. number
  54. >,
  55. lastResetAt: new Date(),
  56. uptime: 0,
  57. }
  58. // Initialize signers
  59. this.initializeSigners()
  60. }
  61. /**
  62. * Load configuration file
  63. */
  64. async loadConfig(filePath: string): Promise<LoadResult> {
  65. const startTime = Date.now()
  66. try {
  67. // Load configuration using ConfigLoader
  68. const result = await this.configLoader.loadConfig(filePath)
  69. // Update metrics
  70. const loadTime = Date.now() - startTime
  71. this.metrics.configLoadTime = loadTime
  72. if (result.success) {
  73. // Clear existing accounts
  74. this.accounts.clear()
  75. // Add loaded accounts to internal storage
  76. for (const account of result.accounts) {
  77. this.accounts.set(account.id, account)
  78. }
  79. this.metrics.accountCount = this.accounts.size
  80. this.currentConfigPath = filePath
  81. // Validate all accounts have supported platforms
  82. for (const account of result.accounts) {
  83. if (!this.signers.has(account.platform)) {
  84. this.recordError(CredentialErrorCode.PLATFORM_NOT_SUPPORTED)
  85. console.warn(`Platform ${account.platform} not supported for account ${account.id}`)
  86. }
  87. }
  88. } else {
  89. this.recordError(CredentialErrorCode.CONFIG_VALIDATION_FAILED)
  90. }
  91. return result
  92. } catch (error) {
  93. const loadTime = Date.now() - startTime
  94. this.metrics.configLoadTime = loadTime
  95. this.recordError(CredentialErrorCode.CONFIG_NOT_FOUND)
  96. throw new CredentialManagerError(
  97. CredentialErrorCode.CONFIG_NOT_FOUND,
  98. `Failed to load configuration: ${error instanceof Error ? error.message : 'Unknown error'}`,
  99. error,
  100. )
  101. }
  102. }
  103. /**
  104. * Watch configuration file for changes
  105. */
  106. watchConfig(filePath: string, callback: (accounts: Account[]) => void): void {
  107. try {
  108. if (this.isWatching && this.currentConfigPath !== filePath) {
  109. // Stop watching previous file
  110. this.stopWatching()
  111. }
  112. // Set up file watching
  113. this.configLoader.watchConfig(filePath, accounts => {
  114. const startTime = Date.now()
  115. // Update internal account storage
  116. this.accounts.clear()
  117. for (const account of accounts) {
  118. this.accounts.set(account.id, account)
  119. }
  120. this.metrics.accountCount = this.accounts.size
  121. this.metrics.configWatchLatency = Date.now() - startTime
  122. // Call user callback
  123. callback(accounts)
  124. })
  125. this.isWatching = true
  126. this.currentConfigPath = filePath
  127. } catch (error) {
  128. this.recordError(CredentialErrorCode.FILE_WATCH_FAILED)
  129. throw new CredentialManagerError(
  130. CredentialErrorCode.FILE_WATCH_FAILED,
  131. `Failed to watch configuration file: ${error instanceof Error ? error.message : 'Unknown error'}`,
  132. error,
  133. )
  134. }
  135. }
  136. /**
  137. * Stop watching configuration file
  138. */
  139. stopWatching(): void {
  140. try {
  141. this.configLoader.stopWatching()
  142. this.isWatching = false
  143. this.currentConfigPath = undefined
  144. } catch (error) {
  145. console.warn('Error stopping configuration watching:', error)
  146. }
  147. }
  148. /**
  149. * Get account by ID
  150. */
  151. getAccount(accountId: string): Account | null {
  152. if (!accountId || typeof accountId !== 'string') {
  153. return null
  154. }
  155. return this.accounts.get(accountId) || null
  156. }
  157. /**
  158. * List all accounts
  159. */
  160. listAccounts(): Account[] {
  161. return Array.from(this.accounts.values())
  162. }
  163. /**
  164. * Sign message with account
  165. */
  166. async sign(accountId: string, message: Uint8Array): Promise<SignResult> {
  167. const startTime = Date.now()
  168. this.metrics.signOperationsTotal++
  169. try {
  170. // Validate inputs
  171. if (!accountId || typeof accountId !== 'string') {
  172. throw new CredentialManagerError(CredentialErrorCode.ACCOUNT_NOT_FOUND, 'Invalid account ID')
  173. }
  174. if (!message || message.length === 0) {
  175. throw new CredentialManagerError(CredentialErrorCode.INVALID_MESSAGE, 'Message cannot be empty')
  176. }
  177. // Get account
  178. const account = this.getAccount(accountId)
  179. if (!account) {
  180. throw new CredentialManagerError(CredentialErrorCode.ACCOUNT_NOT_FOUND, `Account not found: ${accountId}`)
  181. }
  182. // Check if account is enabled
  183. if (!account.enabled) {
  184. throw new CredentialManagerError(CredentialErrorCode.ACCOUNT_DISABLED, `Account is disabled: ${accountId}`)
  185. }
  186. // Get appropriate signer
  187. const signer = this.signers.get(account.platform)
  188. if (!signer) {
  189. throw new CredentialManagerError(
  190. CredentialErrorCode.PLATFORM_NOT_SUPPORTED,
  191. `Platform not supported: ${account.platform}`,
  192. )
  193. }
  194. // Perform signing
  195. const signature = await signer.sign(message, account.credentials)
  196. const duration = Date.now() - startTime
  197. this.updateSigningMetrics(true, duration)
  198. // Update account last used time
  199. account.lastUsed = new Date()
  200. return {
  201. success: true,
  202. signature,
  203. algorithm: this.getAlgorithmForPlatform(account.platform),
  204. timestamp: new Date(),
  205. duration,
  206. }
  207. } catch (error) {
  208. const duration = Date.now() - startTime
  209. this.updateSigningMetrics(false, duration)
  210. if (error instanceof CredentialManagerError) {
  211. this.recordError(error.code)
  212. return {
  213. success: false,
  214. algorithm: this.getAlgorithmForPlatform(Platform.PACIFICA), // Default
  215. timestamp: new Date(),
  216. duration,
  217. error: error.message,
  218. }
  219. }
  220. this.recordError(CredentialErrorCode.SIGNING_FAILED)
  221. return {
  222. success: false,
  223. algorithm: this.getAlgorithmForPlatform(Platform.PACIFICA), // Default
  224. timestamp: new Date(),
  225. duration,
  226. error: `Signing failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
  227. }
  228. }
  229. }
  230. /**
  231. * Verify signature
  232. */
  233. async verify(accountId: string, message: Uint8Array, signature: string): Promise<boolean> {
  234. const startTime = Date.now()
  235. this.metrics.verifyOperationsTotal++
  236. try {
  237. // Validate inputs
  238. if (!accountId || !message || !signature) {
  239. this.metrics.verifyOperationsFailed++
  240. return false
  241. }
  242. // Get account
  243. const account = this.getAccount(accountId)
  244. if (!account) {
  245. this.metrics.verifyOperationsFailed++
  246. return false
  247. }
  248. // Get appropriate signer
  249. const signer = this.signers.get(account.platform)
  250. if (!signer) {
  251. this.metrics.verifyOperationsFailed++
  252. return false
  253. }
  254. // Get public key for verification
  255. let publicKey: string
  256. try {
  257. publicKey = await this.getPublicKeyForAccount(account)
  258. } catch (error) {
  259. this.metrics.verifyOperationsFailed++
  260. return false
  261. }
  262. // Perform verification
  263. const isValid = await signer.verify(message, signature, publicKey)
  264. const duration = Date.now() - startTime
  265. this.updateVerificationMetrics(isValid, duration)
  266. return isValid
  267. } catch (error) {
  268. const duration = Date.now() - startTime
  269. this.updateVerificationMetrics(false, duration)
  270. return false
  271. }
  272. }
  273. /**
  274. * Get performance metrics
  275. */
  276. getMetrics(): PerformanceMetrics {
  277. // Update uptime
  278. this.metrics.uptime = Date.now() - this.metrics.lastResetAt.getTime()
  279. // Update memory usage if available
  280. if (process.memoryUsage) {
  281. this.metrics.memoryUsage = process.memoryUsage().heapUsed
  282. }
  283. return { ...this.metrics }
  284. }
  285. /**
  286. * Reset performance metrics
  287. */
  288. resetMetrics(): void {
  289. this.metrics = {
  290. configLoadTime: 0,
  291. configWatchLatency: 0,
  292. signOperationsTotal: 0,
  293. signOperationsSuccess: 0,
  294. signOperationsFailed: 0,
  295. averageSignTime: 0,
  296. maxSignTime: 0,
  297. minSignTime: Infinity,
  298. verifyOperationsTotal: 0,
  299. verifyOperationsSuccess: 0,
  300. verifyOperationsFailed: 0,
  301. averageVerifyTime: 0,
  302. memoryUsage: this.metrics.memoryUsage,
  303. accountCount: this.accounts.size,
  304. errorsByCode: {},
  305. lastResetAt: new Date(),
  306. uptime: 0,
  307. }
  308. }
  309. /**
  310. * Health check
  311. */
  312. async healthCheck(): Promise<{
  313. healthy: boolean
  314. components: {
  315. configLoader: boolean
  316. accounts: boolean
  317. signers: Record<Platform, boolean>
  318. }
  319. performance: {
  320. configLoadTime: number
  321. averageSignTime: number
  322. memoryUsage: number
  323. }
  324. timestamp: Date
  325. }> {
  326. const health = {
  327. healthy: true,
  328. components: {
  329. configLoader: true,
  330. accounts: this.accounts.size > 0,
  331. signers: {
  332. [Platform.PACIFICA]: this.signers.has(Platform.PACIFICA),
  333. [Platform.ASTER]: this.signers.has(Platform.ASTER),
  334. [Platform.BINANCE]: this.signers.has(Platform.BINANCE),
  335. },
  336. },
  337. performance: {
  338. configLoadTime: this.metrics.configLoadTime,
  339. averageSignTime: this.metrics.averageSignTime,
  340. memoryUsage: this.metrics.memoryUsage,
  341. },
  342. timestamp: new Date(),
  343. }
  344. // Check overall health
  345. health.healthy =
  346. health.components.configLoader &&
  347. Object.values(health.components.signers).some(Boolean) &&
  348. health.performance.configLoadTime < 100 &&
  349. health.performance.averageSignTime < 50
  350. return health
  351. }
  352. /**
  353. * Initialize platform signers
  354. */
  355. private initializeSigners(): void {
  356. // Initialize Pacifica signer
  357. this.signers.set(Platform.PACIFICA, new PacificaSigner())
  358. // TODO: Initialize other signers when implemented
  359. // this.signers.set(Platform.BINANCE, new BinanceSigner());
  360. // this.signers.set(Platform.ASTER, new AsterSigner());
  361. }
  362. /**
  363. * Get algorithm for platform
  364. */
  365. private getAlgorithmForPlatform(platform: Platform): string {
  366. switch (platform) {
  367. case Platform.PACIFICA:
  368. return 'ed25519'
  369. case Platform.ASTER:
  370. return 'eip191'
  371. case Platform.BINANCE:
  372. return 'hmac-sha256'
  373. default:
  374. return 'unknown'
  375. }
  376. }
  377. /**
  378. * Get public key for account
  379. */
  380. private async getPublicKeyForAccount(account: Account): Promise<string> {
  381. switch (account.platform) {
  382. case Platform.PACIFICA:
  383. if (account.credentials.type === 'ed25519' && 'privateKey' in account.credentials) {
  384. // Derive public key from private key
  385. const { PacificaKeyUtils } = await import('./signers/PacificaSigner')
  386. return PacificaKeyUtils.derivePublicKey(account.credentials.privateKey)
  387. }
  388. break
  389. case Platform.ASTER:
  390. // TODO: Implement Aster public key derivation
  391. break
  392. case Platform.BINANCE:
  393. // HMAC doesn't use public keys for verification
  394. return ''
  395. }
  396. throw new CredentialManagerError(
  397. CredentialErrorCode.ACCOUNT_INVALID_CREDENTIALS,
  398. `Cannot derive public key for platform ${account.platform}`,
  399. )
  400. }
  401. /**
  402. * Update signing metrics
  403. */
  404. private updateSigningMetrics(success: boolean, duration: number): void {
  405. if (success) {
  406. this.metrics.signOperationsSuccess++
  407. } else {
  408. this.metrics.signOperationsFailed++
  409. }
  410. // Update timing metrics
  411. this.metrics.maxSignTime = Math.max(this.metrics.maxSignTime, duration)
  412. this.metrics.minSignTime = Math.min(this.metrics.minSignTime, duration)
  413. // Calculate rolling average
  414. const totalSuccessfulSigns = this.metrics.signOperationsSuccess
  415. if (totalSuccessfulSigns > 0) {
  416. this.metrics.averageSignTime =
  417. (this.metrics.averageSignTime * (totalSuccessfulSigns - 1) + duration) / totalSuccessfulSigns
  418. }
  419. }
  420. /**
  421. * Update verification metrics
  422. */
  423. private updateVerificationMetrics(success: boolean, duration: number): void {
  424. if (success) {
  425. this.metrics.verifyOperationsSuccess++
  426. } else {
  427. this.metrics.verifyOperationsFailed++
  428. }
  429. // Calculate rolling average for verification time
  430. const totalVerifications = this.metrics.verifyOperationsTotal
  431. if (totalVerifications > 0) {
  432. this.metrics.averageVerifyTime =
  433. (this.metrics.averageVerifyTime * (totalVerifications - 1) + duration) / totalVerifications
  434. }
  435. }
  436. /**
  437. * Record error in metrics
  438. */
  439. private recordError(errorCode: CredentialErrorCode): void {
  440. this.metrics.errorsByCode[errorCode] = (this.metrics.errorsByCode[errorCode] || 0) + 1
  441. }
  442. /**
  443. * Validate account credentials
  444. */
  445. validateAccount(accountId: string): {
  446. isValid: boolean
  447. errors: string[]
  448. warnings: string[]
  449. } {
  450. const account = this.getAccount(accountId)
  451. if (!account) {
  452. return {
  453. isValid: false,
  454. errors: [`Account not found: ${accountId}`],
  455. warnings: [],
  456. }
  457. }
  458. return this.validator.validateAccountConfig(account)
  459. }
  460. /**
  461. * Get supported platforms
  462. */
  463. getSupportedPlatforms(): Platform[] {
  464. return Array.from(this.signers.keys())
  465. }
  466. /**
  467. * Check if platform is supported
  468. */
  469. isPlatformSupported(platform: Platform): boolean {
  470. return this.signers.has(platform)
  471. }
  472. }