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