Source

services/vault.service.js

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();