import app from './app'; import { env, initEnv } from './config/env'; import { refreshAllKeys, startKeyRotation, stopKeyRotation } from './services/key-rotation.service'; import { isCryptoReady, decryptMnemonic, encryptMnemonic } from './services/crypto.service'; import { db } from './config/database'; import { pingRedis, closeRedis } from './config/redis'; import { logger } from './lib/logger'; // Global error handlers — иначе unhandled errors идут в stderr без sanitization (leak secrets) process.on('unhandledRejection', (reason: any) => { logger.error(`Unhandled rejection: ${reason?.stack || reason?.message || reason}`); }); process.on('uncaughtException', (err: Error) => { logger.error(`Uncaught exception: ${err.stack || err.message}`); // Process state could be corrupt — exit cleanly process.exit(1); }); async function main() { logger.info(`Wallet service instance started with id ${logger.instanceId}`); await initEnv(); const refreshResult = await refreshAllKeys(); if (!refreshResult.ok) { logger.error(`Initial Vault refresh failed: ${refreshResult.reason}. Refusing to start.`); process.exit(1); } // Custodial: без master-key сервис не может расшифровать ни одну мнемонику — fail fast. if (!isCryptoReady()) { logger.error('Crypto master key not loaded — refusing to start (custodial wallets require it)'); process.exit(1); } // Integrity self-test: если в БД уже есть encrypted_mnemonic, master-key должен их декриптить. // Иначе мы стартовали с НЕПРАВИЛЬНЫМ ключом (например после потери Vault dev-mode state) — // и любой /send или /sign будет тихо валиться с GCM auth error. Лучше упасть сразу. await runCryptoIntegritySelfTest(); // KeyDB / Redis ping — idempotency critical для money flow; fail-fast если недоступен. try { await pingRedis(); logger.info('KeyDB/Redis self-test: PASSED'); } catch (err: any) { logger.error(`KeyDB/Redis ping failed: ${err.message}. Refusing to start (idempotency unavailable).`); process.exit(1); } startKeyRotation(); const server = app.listen(env.port, () => { logger.info(`Server running on port ${env.port}`); }); const shutdown = (signal: string) => { logger.info(`${signal} received, shutting down gracefully`); stopKeyRotation(); void closeRedis(); server.close(() => process.exit(0)); // Force exit if shutdown takes too long setTimeout(() => process.exit(1), 10_000).unref(); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); } async function runCryptoIntegritySelfTest(): Promise { // H4 — Two checks: // (a) AES round-trip ALWAYS (даже на пустой DB): encrypt PROBE → decrypt → equal. // Защита от corrupted master-key buffer (wrong size, zeros, etc.) // (b) Если в БД есть encrypted_mnemonic — пытаемся декриптить (sample 3 rows). // Защита от master-key drift (Vault rotation accidental). // (a) AES round-trip try { const probe = 'self-test-PROBE-string-with-some-length-for-realism'; const ct = encryptMnemonic(probe); const pt = decryptMnemonic(ct); if (pt !== probe) { logger.error('Crypto self-test: AES round-trip MISMATCH — master-key buffer corrupt'); process.exit(1); } } catch (err: any) { logger.error(`Crypto self-test: AES round-trip failed: ${err?.message}`); process.exit(1); } // (b) Existing-blob compatibility try { const rows = await db('users') .whereNotNull('encrypted_mnemonic') .select('encrypted_mnemonic') .limit(3); if (rows.length === 0) { logger.info('Crypto self-test: PASSED (round-trip OK, no existing blobs to check)'); return; } for (const row of rows) { try { decryptMnemonic(row.encrypted_mnemonic); } catch (err: any) { logger.error( 'Crypto self-test: FAILED — master-key DOES NOT match stored encrypted_mnemonic. ' + 'Likely cause: Vault state was lost (dev-mode in-memory) and a different key was seeded. ' + 'Recovery: restore seed-cache.env from .seed-backup/ OR wipe DB with `docker compose down -v`. ' + `Underlying: ${err?.message}`, ); process.exit(1); } } logger.info(`Crypto self-test: PASSED (round-trip OK + ${rows.length} existing blob(s) decryptable)`); } catch (err: any) { logger.error(`Crypto self-test: could not query DB: ${err?.message}`); process.exit(1); } } main().catch((err) => { logger.error(`Failed to start: ${err.message}`); process.exit(1); });