Files
cryptowallet/apps/api/src/index.ts
ZOMBIIIIIII 762a46871b init2222
2026-05-13 12:07:48 +03:00

125 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> {
// 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);
});