Source

config/vault-secrets.js

import { env } from "@config/env";
import { vaultService } from "@services/vault.service";
import { getLogger } from "@utils/asyncLocalStorage";
/**
 * Vault-backed secrets that are loaded after Vault initialization
 * @category Vault
 */
export class VaultSecrets {
    /**
     * The instance of the VaultSecrets class
     * @category Config
     */
    static instance = null;
    secrets = new Map();
    isLoaded = false;
    /**
     * The dynamic configuration for secrets to load
     * @category Config
     */
    secretsConfig = [
        {
            vaultKey: "jwt",
            keys: [
                {
                    name: "JWT_SECRET",
                    envFallback: env.JWT_SECRET,
                    defaultValue: "secret",
                    required: true,
                },
            ],
        },
        // Add more secrets here as needed
        // {
        //   vaultKey: "database",
        //   keys: [
        //     {
        //       name: "DB_PASSWORD",
        //       envFallback: process.env.DB_PASSWORD,
        //       required: true,
        //     },
        //     {
        //       name: "DB_HOST",
        //       envFallback: process.env.DB_HOST,
        //       defaultValue: "localhost",
        //     }
        //   ]
        // },
        // {
        //   vaultKey: "api",
        //   keys: [
        //     {
        //       name: "API_KEY",
        //       envFallback: process.env.API_KEY,
        //     }
        //   ]
        // },
    ];
    constructor() { }
    /**
     * Get the instance of the VaultSecrets class
     * @category Config
     * @returns {VaultSecrets} The instance of the VaultSecrets class
     */
    static getInstance() {
        if (!VaultSecrets.instance) {
            VaultSecrets.instance = new VaultSecrets();
        }
        return VaultSecrets.instance;
    }
    /**
     * Load secrets from Vault dynamically based on configuration
     */
    async loadSecrets() {
        const logger = getLogger();
        try {
            logger.info(`Loading ${this.secretsConfig.length} secrets from Vault...`);
            const results = {
                success: 0,
                failed: 0,
                fallbacks: 0,
            };
            // Process each secret configuration
            for (const config of this.secretsConfig) {
                try {
                    await this.loadVaultSecrets(config, results);
                }
                catch (error) {
                    logger.error({
                        vaultKey: config.vaultKey,
                        error: error instanceof Error ? error.message : "Unknown error",
                    }, `Failed to load vault secrets: ${config.vaultKey}`);
                    // Handle fallback for all keys in this vault
                    this.handleVaultFallback(config, results);
                }
            }
            this.isLoaded = true;
            logger.info({
                total: this.secretsConfig.length,
                success: results.success,
                fallbacks: results.fallbacks,
                failed: results.failed,
            }, "Secrets loading completed");
            // Check if any required secrets failed completely
            this.validateRequiredSecrets();
        }
        catch (error) {
            logger.error({ error: error instanceof Error ? error.message : "Unknown error" }, "Critical error during secrets loading");
            // Load all fallback values
            this.loadAllFallbacks();
            this.isLoaded = true;
        }
    }
    /**
     * Load secrets from a vault key based on its configuration
     * @category Config
     * @param {ISecretConfig} config - The configuration for the secrets to load
     * @param {object} results - The results of the loading process
     * @param {number} results.success - The number of secrets that were loaded successfully
     * @param {number} results.failed - The number of secrets that failed to load
     * @param {number} results.fallbacks - The number of secrets that were loaded from fallback values
     * @returns {Promise<void>} A promise that resolves when the secrets are loaded
     */
    async loadVaultSecrets(config, results) {
        const logger = getLogger();
        try {
            // Attempt to load from Vault
            const vaultData = await vaultService.getSecret(config.vaultKey);
            if (vaultData && typeof vaultData === "object") {
                const vaultKeys = Object.keys(vaultData);
                // Process each key in the vault data
                for (const secretKey of config.keys) {
                    try {
                        // Try to find the secret value in vault data
                        let secretValue;
                        // Look for the key name in vault data (case-insensitive)
                        const vaultKeyName = vaultKeys.find((key) => key.toLowerCase() === secretKey.name.toLowerCase());
                        if (vaultKeyName && vaultData[vaultKeyName]) {
                            secretValue = vaultData[vaultKeyName];
                        }
                        if (secretValue) {
                            this.secrets.set(secretKey.name, secretValue);
                            results.success++;
                            logger.info(`${secretKey.name} loaded from Vault (${config.vaultKey})`);
                        }
                        else {
                            // Key not found in vault data, use fallback
                            this.handleSecretKeyFallback(secretKey, results);
                        }
                    }
                    catch (error) {
                        logger.warn({
                            secretName: secretKey.name,
                            vaultKey: config.vaultKey,
                            error: error instanceof Error ? error.message : "Unknown error",
                        }, `Failed to process secret ${secretKey.name}, using fallback`);
                        this.handleSecretKeyFallback(secretKey, results);
                    }
                }
                return;
            }
            // If we reach here, vault data was not usable
            this.handleVaultFallback(config, results);
        }
        catch (error) {
            // Vault access failed, use fallback for all keys
            logger.warn({
                vaultKey: config.vaultKey,
                error: error instanceof Error ? error.message : "Unknown error",
            }, `Vault access failed for ${config.vaultKey}, using fallbacks`);
            this.handleVaultFallback(config, results);
        }
    }
    /**
     * Handle fallback for a single secret key
     * @category Config
     * @param {ISecretKey} secretKey - The secret key to handle the fallback for
     * @param {object} results - The results of the loading process
     * @param {number} results.success - The number of secrets that were loaded successfully
     * @param {number} results.failed - The number of secrets that failed to load
     * @param {number} results.fallbacks - The number of secrets that were loaded from fallback values
     * @returns {void} A promise that resolves when the secrets are loaded
     */
    handleSecretKeyFallback(secretKey, results) {
        const logger = getLogger();
        if (secretKey.envFallback) {
            this.secrets.set(secretKey.name, secretKey.envFallback);
            results.fallbacks++;
            logger.info(`${secretKey.name} using environment fallback`);
        }
        else if (secretKey.defaultValue) {
            this.secrets.set(secretKey.name, secretKey.defaultValue);
            results.fallbacks++;
            logger.warn(`${secretKey.name} using default value`);
        }
        else {
            results.failed++;
            logger.error(`${secretKey.name} has no fallback value`);
            if (secretKey.required) {
                throw new Error(`Required secret ${secretKey.name} could not be loaded and has no fallback`);
            }
        }
    }
    /**
     * Handle fallback for all keys in a vault configuration
     * @category Config
     * @param {ISecretConfig} config - The configuration for the secrets to load
     * @param {object} results - The results of the loading process
     * @param {number} results.success - The number of secrets that were loaded successfully
     * @param {number} results.failed - The number of secrets that failed to load
     * @param {number} results.fallbacks - The number of secrets that were loaded from fallback values
     * @returns {void} A promise that resolves when the secrets are loaded
     */
    handleVaultFallback(config, results) {
        for (const secretKey of config.keys) {
            this.handleSecretKeyFallback(secretKey, results);
        }
    }
    /**
     * Validate that all required secrets were loaded
     */
    validateRequiredSecrets() {
        const missingRequired = [];
        for (const config of this.secretsConfig) {
            for (const secretKey of config.keys) {
                if (secretKey.required && !this.secrets.has(secretKey.name)) {
                    missingRequired.push(secretKey.name);
                }
            }
        }
        if (missingRequired.length > 0) {
            throw new Error(`Missing required secrets: ${missingRequired.join(", ")}`);
        }
    }
    /**
     * Load all fallback values (emergency fallback)
     */
    loadAllFallbacks() {
        const logger = getLogger();
        logger.warn("Loading all fallback values due to Vault failure");
        for (const config of this.secretsConfig) {
            for (const secretKey of config.keys) {
                const fallbackValue = secretKey.envFallback || secretKey.defaultValue;
                if (fallbackValue) {
                    this.secrets.set(secretKey.name, fallbackValue);
                    logger.info(`${secretKey.name} loaded from fallback`);
                }
                else if (secretKey.required) {
                    logger.error(`Required secret ${secretKey.name} has no fallback`);
                }
            }
        }
    }
    /**
     * Get a secret value
     * @category Config
     * @param {string} key - The key of the secret to get
     * @returns {string | undefined} The value of the secret
     */
    get(key) {
        const logger = getLogger();
        // if (!this.isLoaded) {
        //   throw new Error("Secrets not loaded yet. Call loadSecrets() first.");
        // }
        try {
            return this.secrets.get(key) || env[key] || "";
        }
        catch (error) {
            logger.error({
                key,
                error: error instanceof Error ? error.message : "Unknown error",
            }, "Failed to get secret from Vault");
            return env[key] || "";
        }
    }
    /**
     * Check if secrets are loaded
     * @category Config
     * @returns {boolean} True if the secrets are loaded, false otherwise
     */
    isSecretsLoaded() {
        return this.isLoaded;
    }
    /**
     * Add a new secret configuration dynamically
     * @category Config
     * @param {ISecretConfig} config - The configuration for the secrets to add
     * @returns {void} A promise that resolves when the secrets are added
     */
    addISecretConfig(config) {
        this.secretsConfig.push(config);
    }
    /**
     * Get all configured secret keys
     * @category Config
     * @returns {string[]} An array of all configured secret keys
     */
    getConfiguredSecrets() {
        const allKeys = [];
        for (const config of this.secretsConfig) {
            for (const secretKey of config.keys) {
                allKeys.push(secretKey.name);
            }
        }
        return allKeys;
    }
    /**
     * Get configuration for a specific secret
     * @category Config
     * @param {string} secretName - The name of the secret to get the configuration for
     * @returns {ISecretKey | undefined} The configuration for the secret, or undefined if not found
     */
    getISecretConfig(secretName) {
        for (const config of this.secretsConfig) {
            const secretKey = config.keys.find((key) => key.name === secretName);
            if (secretKey) {
                return secretKey;
            }
        }
        return undefined;
    }
    /**
     * Get vault configuration for a specific secret
     * @category Config
     * @param {string} secretName - The name of the secret to get the vault configuration for
     * @returns {ISecretConfig | undefined} The vault configuration for the secret, or undefined if not found
     */
    getVaultConfig(secretName) {
        for (const config of this.secretsConfig) {
            const secretKey = config.keys.find((key) => key.name === secretName);
            if (secretKey) {
                return config;
            }
        }
        return undefined;
    }
    /**
     * Check if a secret exists in memory
     * @category Config
     * @param {string} key - The key of the secret to check
     * @returns {boolean} True if the secret exists in memory, false otherwise
     */
    has(key) {
        return this.secrets.has(key);
    }
    /**
     * Get all loaded secret keys (for debugging)
     * @category Config
     * @returns {string[]} An array of all loaded secret keys
     */
    getLoadedKeys() {
        return Array.from(this.secrets.keys());
    }
}
export const vaultSecrets = VaultSecrets.getInstance();