import { env } from "@config/env";
import { logger } from "@config/logger";
import { vaultService } from "@services/vault.service";
/**
* Vault-backed secrets manager responsible for loading and caching sensitive values.
* @category Config
*/
export class VaultSecrets {
static instance = null;
secrets = new Map();
isLoaded = false;
// Dynamic configuration for secrets to load
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() { }
/**
* Retrieve the singleton instance of the secrets manager.
* @category Config
* @returns {VaultSecrets} Vault secrets manager instance.
*/
static getInstance() {
if (!VaultSecrets.instance) {
VaultSecrets.instance = new VaultSecrets();
}
return VaultSecrets.instance;
}
/**
* Load secrets from Vault dynamically based on configuration.
* @category Config
* @returns {Promise<void>} Resolves when all secrets are processed.
*/
async loadSecrets() {
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 specific Vault key based on its configuration.
* @category Config
* @param {ISecretConfig} config Vault configuration definition.
* @param {object} results Aggregate counters.
* @param {number} results.success Count of successfully loaded secrets.
* @param {number} results.failed Count of failures.
* @param {number} results.fallbacks Count of fallback usages.
* @returns {Promise<void>} Resolves when the vault section is processed.
*/
async loadVaultSecrets(config, results) {
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 Definition of the secret key.
* @param {object} results Aggregate counters.
* @param {number} results.success Count of successfully loaded secrets.
* @param {number} results.failed Count of failures.
* @param {number} results.fallbacks Count of fallback usages.
*/
handleSecretKeyFallback(secretKey, results) {
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 Vault configuration definition.
* @param {object} results Aggregate counters.
* @param {number} results.success Count of successfully loaded secrets.
* @param {number} results.failed Count of failures.
* @param {number} results.fallbacks Count of fallback usages.
*/
handleVaultFallback(config, results) {
for (const secretKey of config.keys) {
this.handleSecretKeyFallback(secretKey, results);
}
}
/**
* Validate that all required secrets were loaded.
* @category Config
* @throws {Error} When required secrets are missing.
*/
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) when Vault fails entirely.
* @category Config
* @returns {void}
*/
loadAllFallbacks() {
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 loaded secret value or fallback to env variables.
* @category Config
* @param {string} key Secret identifier.
* @returns {string|number} Secret value.
*/
get(key) {
const secret = this.secrets.get(key);
if (!secret) {
return env[key] || "";
}
return secret;
}
/**
* Check if secrets have been loaded.
* @category Config
* @returns {boolean} True when the cache has been populated.
*/
isSecretsLoaded() {
return this.isLoaded;
}
/**
* Add a new secret configuration dynamically.
* @category Config
* @param {ISecretConfig} config Configuration block to append.
*/
addISecretConfig(config) {
this.secretsConfig.push(config);
}
/**
* Get all configured secret key names.
* @category Config
* @returns {string[]} Array of secret identifiers.
*/
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 Secret identifier.
* @returns {ISecretKey | undefined} Matching secret definition, if any.
*/
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 Secret identifier.
* @returns {ISecretConfig | undefined} Matching vault configuration, if any.
*/
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 Secret identifier.
* @returns {boolean} True when the secret is cached.
*/
has(key) {
return this.secrets.has(key);
}
/**
* Get all loaded secret keys (for debugging).
* @category Config
* @returns {string[]} Array of cached secret names.
*/
getLoadedKeys() {
return Array.from(this.secrets.keys());
}
}
export const vaultSecrets = VaultSecrets.getInstance();
Source