import { env } from "@config/env";
import { Client } from "@litehex/node-vault";
import { getLogger } from "@utils/asyncLocalStorage";
/**
* Vault Service
* @category Services
*/
export class VaultService {
client = null;
isInitialized = false;
isInTestMode = false;
/**
* Initializes the HashiCorp Vault service.
* @category Services
* @throws {Error} If the Vault client cannot be initialized or authentication fails.
*/
async initialize() {
const logger = getLogger();
try {
logger.info("Initializing HashiCorp Vault Service..." +
JSON.stringify({
url: env.VAULT_URL,
}));
// Initialize vault client
this.client = new Client({
apiVersion: "v1", // @litehex/node-vault uses v1 API
endpoint: env.VAULT_URL,
});
// Authenticate if using AppRole
if (env.VAULT_ROLE_ID && env.VAULT_SECRET_ID && !env.VAULT_TOKEN) {
await this.authenticateWithAppRole();
}
// Test connection by checking vault health
if (this.client) {
try {
// Use a simple test operation instead of status check
logger.info("Vault client initialized successfully");
}
catch (error) {
logger.warn(error, "Could not verify vault connection during initialization");
}
}
// Test basic operations
// this.isInTestMode = true;
// await this.testVaultOperations();
// this.isInTestMode = false;
this.isInitialized = true;
logger.info("HashiCorp Vault Service initialized successfully");
}
catch (error) {
logger.error({ error: error instanceof Error ? error.message : "Unknown error" }, "Failed to initialize HashiCorp Vault Service");
throw error;
}
}
/**
* Authenticates the Vault client using the AppRole method.
* @category Services
* @throws {Error} If the Vault client is not initialized, authentication fails, or no token is received.
*/
async authenticateWithAppRole() {
const logger = getLogger();
if (!this.client) {
throw new Error("Vault client not initialized");
}
try {
logger.info("Authenticating with HashiCorp Vault using AppRole...");
const response = await this.client.write({
path: "auth/approle/login",
data: {
role_id: env.VAULT_ROLE_ID,
secret_id: env.VAULT_SECRET_ID,
},
});
if (response.data && typeof response.data === "object" && "auth" in response.data) {
this.client.token = response.data.auth.client_token;
logger.info("Successfully authenticated with HashiCorp Vault using AppRole");
}
else {
logger.debug({
error: {
name: response.error?.name,
message: response.error?.message,
cause: response.error?.cause,
stack: response.error?.stack,
},
data: response.data,
}, "AppRole login response");
throw new Error("No client token received from AppRole login");
}
}
catch (error) {
logger.error({ error: error instanceof Error ? error.message : "Unknown error" }, "Failed to authenticate with HashiCorp Vault using AppRole");
throw error;
}
}
/**
* Stores a secret in HashiCorp Vault.
* @category Services
* @param {string} key - The key under which to store the secret.
* @param {Record<string, string>} value - The secret data as a record of string key-value pairs.
* @returns {Promise<void>}
* @throws {Error} If the Vault service is not initialized or if storing the secret fails.
*/
async setSecret(key, value) {
const logger = getLogger();
if ((!this.isInitialized && !this.isInTestMode) || !this.client) {
throw new Error("Vault service is not initialized");
}
try {
const res = await this.client.kv2.write({
mountPath: env.VAULT_MOUNT_PATH,
path: key,
data: value,
});
logger.info({ key, res }, "Secret stored in HashiCorp Vault");
}
catch (error) {
logger.error({
key,
error: error instanceof Error ? error.message : "Unknown error",
}, "Failed to store secret in HashiCorp Vault");
throw error;
}
}
/**
* Retrieves a secret from HashiCorp Vault, optionally using Redis cache.
* @category Services
* @param {string} optionalKey - The key of the secret to retrieve. Defaults to the Vault mount path if not provided.
* @returns {Promise<Record<string, any> | null>} - The secret data as a key-value record, or `null` if not found.
* @throws {Error} If the Vault service is not initialized or if retrieving the secret fails.
*/
async getSecret(optionalKey) {
const logger = getLogger();
if ((!this.isInitialized && !this.isInTestMode) || !this.client) {
throw new Error("Vault service is not initialized");
}
const key = optionalKey || env.VAULT_MOUNT_PATH;
try {
// Fetch directly from Vault
const response = await this.client.kv2.read({
mountPath: env.VAULT_MOUNT_PATH,
path: key,
});
if (!response || !response.data) {
return null;
}
// Handle different API versions
let secretValue = response.data?.data.data;
if (secretValue === undefined) {
return null;
}
return secretValue;
}
catch (error) {
logger.error({
key,
error: error instanceof Error ? error.message : "Unknown error",
}, "Failed to retrieve secret from HashiCorp Vault");
throw error;
}
}
/**
* Deletes a secret from HashiCorp Vault.
* @category Services
* @param {string} key - The key of the secret to delete.
* @returns {Promise<boolean>} - Returns `true` if the deletion succeeded.
* @throws {Error} If the Vault service is not initialized or if deleting the secret fails.
*/
async deleteSecret(key) {
const logger = getLogger();
if ((!this.isInitialized && !this.isInTestMode) || !this.client) {
throw new Error("Vault service is not initialized");
}
try {
// KV v2 uses deleteLatest operation
await this.client.kv2.deleteLatest({
mountPath: env.VAULT_MOUNT_PATH,
path: key,
});
return true;
}
catch (error) {
// Handle 404 errors (secret not found) as success
logger.error({
key,
error: error instanceof Error ? error.message : "Unknown error",
}, "Failed to delete secret from HashiCorp Vault");
throw error;
}
}
/**
* Checks if a secret exists in HashiCorp Vault.
* @category Services
* @param {string} key - The key of the secret to check.
* @returns {Promise<boolean>} - `true` if the secret exists, `false` otherwise.
* @throws {Error} If the Vault service is not initialized or if checking the secret fails.
*/
async hasSecret(key) {
const logger = getLogger();
if ((!this.isInitialized && !this.isInTestMode) || !this.client) {
throw new Error("Vault service is not initialized");
}
try {
const secretValue = await this.getSecret(key);
return secretValue !== null;
}
catch (error) {
logger.error({
key,
error: error instanceof Error ? error.message : "Unknown error",
}, "Failed to check secret existence in HashiCorp Vault");
throw error;
}
}
/**
* Lists all keys in the HashiCorp Vault.
* @category Services
* @returns {Promise<string[]>} - An array of key names in the Vault.
* @throws {Error} If the Vault service is not initialized or if listing the keys fails.
*/
async listKeys() {
const logger = getLogger();
if ((!this.isInitialized && !this.isInTestMode) || !this.client) {
throw new Error("Vault service is not initialized");
}
try {
const response = await this.client.kv2.list({
mountPath: env.VAULT_MOUNT_PATH,
path: "",
});
const keys = response.data?.data.keys;
if (!response || !response.data || !keys) {
return [];
}
// Filter out test/system keys
const filteredKeys = keys.filter((key) => !key.startsWith("_vault_"));
return filteredKeys;
}
catch (error) {
logger.error({
error: error instanceof Error ? error.message : "Unknown error",
}, "Failed to list HashiCorp Vault keys");
throw error;
}
}
/**
* Closes the Vault service connections and performs cleanup.
* @category Services
* @throws {Error} If an error occurs during the cleanup process.
*/
close() {
const logger = getLogger();
try {
logger.info("Closing HashiCorp Vault Service...");
if (this.client) {
// @litehex/node-vault doesn't need explicit cleanup
this.client = null;
}
this.isInitialized = false;
logger.info("HashiCorp Vault Service closed successfully");
}
catch (error) {
logger.error({ error: error instanceof Error ? error.message : "Unknown error" }, "Error closing HashiCorp Vault Service");
throw error;
}
}
/**
* Checks if HashiCorp Vault is healthy and accessible.
* @category Services
* @returns {Promise<boolean>} - Returns `true` if Vault is healthy and accessible, `false` otherwise.
*/
async healthCheck() {
const logger = getLogger();
try {
if (!this.isInitialized || !this.client) {
return false;
}
// Test basic vault connectivity
try {
// Try a simple operation to verify connectivity
await this.client.kv2.read({
mountPath: env.VAULT_MOUNT_PATH,
path: "_health_check_test",
});
}
catch (error) {
logger.error({
error: error instanceof Error ? error.message : "Unknown error",
}, "Vault connectivity test failed");
return false;
}
// Test basic operations
const testKey = "_vault_health_check";
const testValue = { health: "health-check-" + Date.now() };
await this.setSecret(testKey, testValue);
const retrievedValue = await this.getSecret(testKey);
await this.deleteSecret(testKey);
return JSON.stringify(retrievedValue) === JSON.stringify(testValue);
}
catch (error) {
logger.error({ error: error instanceof Error ? error.message : "Unknown error" }, "HashiCorp Vault health check failed");
return false;
}
}
}
// Export singleton instance
export const vaultService = new VaultService();
Source