fix: update for main logic

This commit is contained in:
2026-05-01 12:49:24 +03:00
parent b61739cae8
commit f1a68a7bf8
8 changed files with 314 additions and 177 deletions

View File

@@ -22,9 +22,16 @@ export interface RabbitMqSecret {
password: string;
}
export interface EthereumSecret {
rpcUrl?: string;
hotWalletPrivateKey?: string;
usdtContractAddress?: string;
}
export interface BootstrappedSecrets {
database: DatabaseSecret;
rabbitmq: RabbitMqSecret;
ethereum?: EthereumSecret;
}
export class VaultClient {
@@ -36,7 +43,7 @@ export class VaultClient {
private token?: string;
constructor(options: VaultClientOptions) {
this.baseUrl = options.addr.replace(/\/+$/, "");
this.baseUrl = options.addr.replace(/\/+$/, '');
this.mountPoint = options.mountPoint;
this.roleId = options.roleId;
this.secretId = options.secretId;
@@ -45,58 +52,71 @@ export class VaultClient {
async bootstrap(): Promise<BootstrappedSecrets> {
await this.login();
const [database, rabbitmq] = await Promise.all([
this.readSecret("database"),
this.readSecret("rabbitmq")
const [database, rabbitmq, ethereum] = await Promise.all([
this.readSecret('database'),
this.readSecret('rabbitmq'),
this.readOptionalSecret('ethereum')
]);
return {
database: parseDatabaseSecret(database),
rabbitmq: parseRabbitMqSecret(rabbitmq)
rabbitmq: parseRabbitMqSecret(rabbitmq),
ethereum: ethereum ? parseEthereumSecret(ethereum) : undefined
};
}
private async login(): Promise<void> {
const response = await this.fetch(`${this.baseUrl}/v1/auth/approle/login`, {
method: "POST",
headers: { "content-type": "application/json" },
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ role_id: this.roleId, secret_id: this.secretId })
});
const json = (await response.json()) as { auth?: { client_token?: string } };
const token = json.auth?.client_token;
if (!token) {
throw new Error("Vault AppRole login did not return a client_token");
throw new Error('Vault AppRole login did not return a client_token');
}
this.token = token;
}
private async readSecret(path: string): Promise<Record<string, unknown>> {
if (!this.token) {
throw new Error("Vault token is missing; call bootstrap() first");
throw new Error('Vault token is missing; call bootstrap() first');
}
const url = `${this.baseUrl}/v1/${this.mountPoint}/data/${path}`;
const response = await this.fetch(url, {
method: "GET",
headers: { "X-Vault-Token": this.token }
method: 'GET',
headers: { 'X-Vault-Token': this.token }
});
const json = (await response.json()) as { data?: { data?: Record<string, unknown> } };
const data = json.data?.data;
if (!data || typeof data !== "object") {
if (!data || typeof data !== 'object') {
throw new Error(`Vault secret ${this.mountPoint}/${path} is empty or malformed`);
}
return data;
}
private async readOptionalSecret(path: string): Promise<Record<string, unknown> | null> {
try {
return await this.readSecret(path);
} catch (error) {
if (error instanceof Error && error.message.includes('HTTP 404')) {
return null;
}
throw error;
}
}
private async fetch(url: string, init: RequestInit): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await fetch(url, { ...init, signal: controller.signal });
if (!response.ok) {
const text = await response.text().catch(() => "");
const text = await response.text().catch(() => '');
throw new Error(`Vault request to ${url} failed with HTTP ${response.status}: ${text}`);
}
return response;
@@ -106,37 +126,62 @@ export class VaultClient {
}
}
function parseDatabaseSecret(raw: Record<string, unknown>): DatabaseSecret {
return {
host: requireString(raw, "host", "database"),
port: requirePort(raw, "port", "database"),
name: requireString(raw, "name", "database"),
user: requireString(raw, "user", "database"),
password: requireString(raw, "password", "database")
host: requireString(raw, 'host', 'database'),
port: requirePort(raw, 'port', 'database'),
name: requireString(raw, 'name', 'database'),
user: requireString(raw, 'user', 'database'),
password: requireString(raw, 'password', 'database')
};
}
function parseRabbitMqSecret(raw: Record<string, unknown>): RabbitMqSecret {
return {
host: requireString(raw, "host", "rabbitmq"),
port: requirePort(raw, "port", "rabbitmq"),
vhost: requireString(raw, "vhost", "rabbitmq"),
user: requireString(raw, "user", "rabbitmq"),
password: requireString(raw, "password", "rabbitmq")
host: requireString(raw, 'host', 'rabbitmq'),
port: requirePort(raw, 'port', 'rabbitmq'),
vhost: requireString(raw, 'vhost', 'rabbitmq'),
user: requireString(raw, 'user', 'rabbitmq'),
password: requireString(raw, 'password', 'rabbitmq')
};
}
function parseEthereumSecret(raw: Record<string, unknown>): EthereumSecret {
return {
rpcUrl: optionalString(raw, 'rpc_url', 'ethereum'),
hotWalletPrivateKey: optionalString(raw, 'hot_wallet_private_key', 'ethereum'),
usdtContractAddress: optionalString(raw, 'usdt_contract_address', 'ethereum')
};
}
function requireString(raw: Record<string, unknown>, key: string, secretName: string): string {
const value = raw[key];
if (typeof value !== "string" || !value) {
if (typeof value !== 'string' || !value) {
throw new Error(`Vault secret '${secretName}' is missing string field '${key}'`);
}
return value;
}
function optionalString(raw: Record<string, unknown>, key: string, secretName: string): string | undefined {
const value = raw[key];
if (value === undefined || value === null || value === '') {
return undefined;
}
if (typeof value !== 'string') {
throw new Error(`Vault secret '${secretName}' field '${key}' must be a string`);
}
return value;
}
function requirePort(raw: Record<string, unknown>, key: string, secretName: string): number {
const value = raw[key];
const numeric = typeof value === "number" ? value : Number(value);
const numeric = typeof value === 'number' ? value : Number(value);
if (!Number.isFinite(numeric) || numeric <= 0 || numeric > 65535) {
throw new Error(`Vault secret '${secretName}' has invalid port '${String(value)}'`);
}