deploy: POST /api/wallets + full swagger

This commit is contained in:
ZOMBIIIIIII
2026-05-03 20:01:58 +03:00
parent 59a7d1d9ca
commit 295c3a9d6d
27 changed files with 1994 additions and 430 deletions

View File

@@ -1,7 +1,7 @@
import { env, getVaultToken } from '../config/env';
import { vaultAppRoleLogin } from '../config/vault';
import { loadJwtKeysFromVault } from './jwt.service';
import { loadCsrfSecret } from './csrf.service';
import { fetchJwtKeysFromVault, swapKeyMap, getKeyMapSize } from './jwt.service';
import { fetchCsrfConfig, swapCsrfConfig } from './csrf.service';
import { logger } from '../lib/logger';
const DEFAULT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
@@ -10,9 +10,8 @@ let timer: NodeJS.Timeout | null = null;
let currentVaultToken: string | null = null;
/**
* Refresh JWT public keys (active + previous) and CSRF secret from Vault.
* Errors are logged but do NOT throw — старые значения остаются в памяти,
* сервис продолжает работать до следующего успешного refresh.
* Atomic refresh: pre-fetch JWT keys + CSRF config, swap globals only if BOTH succeed.
* При любой ошибке оставляем старые значения в памяти, сервис продолжает работать.
*/
export async function refreshAllKeys(): Promise<void> {
const { addr, roleId, secretId, mount, jwtKidPath, jwtKidsPrefix, csrfPath } = env.vault;
@@ -22,7 +21,7 @@ export async function refreshAllKeys(): Promise<void> {
return;
}
// Use token from initEnv first call; re-login only if we don't have one yet.
// Vault token: используем закэшированный из initEnv, либо логинимся заново
let token = currentVaultToken || getVaultToken();
if (!token) {
const fresh = await vaultAppRoleLogin(addr, roleId, secretId);
@@ -34,19 +33,32 @@ export async function refreshAllKeys(): Promise<void> {
currentVaultToken = fresh;
}
try {
await loadJwtKeysFromVault(addr, token, mount, jwtKidPath, jwtKidsPrefix);
} catch (err: any) {
logger.error(`Failed to refresh JWT keys: ${err.message}`);
// ── Pre-fetch обоих секретов параллельно (НЕ мутируя глобал) ───────────
const jwtPromise = fetchJwtKeysFromVault(addr, token, mount, jwtKidPath, jwtKidsPrefix);
const csrfPromise = csrfPath ? fetchCsrfConfig(addr, token, mount, csrfPath) : Promise.resolve(null);
const [jwtResult, csrfResult] = await Promise.allSettled([jwtPromise, csrfPromise]);
// ── Атомарность: если хоть один обязательный fetch упал — НИЧЕГО не меняем ──
if (jwtResult.status === 'rejected') {
logger.error(`Key refresh ABORTED — JWT keys fetch failed: ${jwtResult.reason?.message || jwtResult.reason}`);
return;
}
if (csrfPath && csrfResult.status === 'rejected') {
logger.error(`Key refresh ABORTED — CSRF fetch failed: ${csrfResult.reason?.message || csrfResult.reason}`);
return;
}
if (csrfPath) {
try {
await loadCsrfSecret(addr, token, mount, csrfPath);
} catch (err: any) {
logger.error(`Failed to refresh CSRF secret: ${err.message}`);
}
// ── Atomic swap (синхронные операции, нельзя прервать) ──────────────────
swapKeyMap(jwtResult.value);
if (csrfResult.status === 'fulfilled' && csrfResult.value) {
swapCsrfConfig(csrfResult.value);
}
logger.info(
`Keys refreshed atomically: JWT keys=${getKeyMapSize()}` +
(csrfPath ? `, CSRF=${csrfResult.status === 'fulfilled' ? 'updated' : 'unchanged'}` : '')
);
}
export function startKeyRotation(intervalMs: number = DEFAULT_INTERVAL_MS): void {
@@ -56,8 +68,7 @@ export function startKeyRotation(intervalMs: number = DEFAULT_INTERVAL_MS): void
void refreshAllKeys().catch((err) =>
logger.error(`Key rotation tick failed: ${err?.message || err}`)
);
// On token expiry Vault will return 403 — we need to re-login.
// Reset cached token so refreshAllKeys re-logs in on next call.
// На каждый тик — invalidate Vault token (он мог истечь), будет re-login
currentVaultToken = null;
}, intervalMs);
logger.info(`Key rotation scheduled (every ${intervalMs}ms)`);