@giancarl021/cli-core-vault-extension
Version:
Plain and secure storage extension for the @giancarl021/cli-core npm package
147 lines (144 loc) • 7.42 kB
JavaScript
import constants from './src/util/constants.js';
import FileStorage from './src/services/FileStorage.js';
import ObjectStorage from './src/services/ObjectStorage.js';
import SecretStorage from './src/services/SecretStorage.js';
import hasStableKeychain from './src/util/hasStableKeychain.js';
/**
* Vault extension for CLI Core.
* Allows easy management of persistent and temporary JSON data storage,
* as well as secret storage based on the OS keychain.
* @param options Options for configuring the vault extension.
* @returns A CLI Core extension object.
*/
function VaultExtension(options = {}) {
/**
* Parse and validate the options provided to the extension.
* @param appName The name of the application using the extension.
* @returns The parsed and validated options.
*/
function _parseOptions(appName) {
const dataPath = options.dataPath ?? `${constants.data.rootPrefix}/.${appName}`;
const tempPath = options.tempPath ?? `${constants.temp.root}/.${appName}`;
if (dataPath === tempPath) {
throw new Error(`Data path and temporary path cannot be the same: ${dataPath}`);
}
if (dataPath.startsWith(tempPath + '/') ||
tempPath.startsWith(dataPath + '/')) {
throw new Error(`Data path and temporary path cannot be subdirectories of each other: ${dataPath} and ${tempPath}`);
}
const initialData = options.initialData ?? {};
const tempInitialData = options.tempInitialData ?? {};
return {
initialData,
tempInitialData,
dataPath,
tempPath,
secretStorage: {
mode: options.secretStorage?.mode ?? 'auto',
encryptionKeyEnvVar: options.secretStorage?.encryptionKeyEnvVar ??
'CLI_CORE_VAULT_KEY'
},
lazyInitialization: options.lazyInitialization ?? true,
destroyTempOnExit: options.destroyTempOnExit ?? false
};
}
/**
* Get a environment variable and check if it is valid (non-empty).
* @param envVarName The name of the environment variable to check for the encryption key.
* @returns The value of the environment variable and its validity.
*/
function _getEnvVar(envVarName) {
const value = process.env[envVarName] || '';
return { value, valid: Boolean(value.trim()), name: envVarName };
}
return {
/**
* Name of the extension.
*/
name: 'vault',
/**
* Build command addons for the vault extension.
* Initializes the storage engines and temporary directory.
*
* @param cliCoreContext Context provided by CLI Core.
* @returns Addons to be added to the CLI Core command.
*/
buildCommandAddons: ({ appName, logger }) => {
const _options = _parseOptions(appName);
const temp = FileStorage(_options.tempPath, _options.lazyInitialization);
logger.debug(`Temporary directory created at ${_options.tempPath}`);
const objectStorage = ObjectStorage(FileStorage(_options.dataPath, _options.lazyInitialization), _options.initialData);
logger.debug(`Data storage initialized at ${_options.dataPath}`);
const tempObjectStorage = ObjectStorage(temp, _options.tempInitialData);
const stableKeychain = hasStableKeychain();
const fileSystemEncryptionKey = _getEnvVar(_options.secretStorage.encryptionKeyEnvVar);
if (!stableKeychain && _options.secretStorage.mode === 'keychain') {
logger.warning(`The current system does not have a stable keychain. Please change the secret storage mode to \`filesystem\` or \`auto\` with a valid encryption key set in the ${fileSystemEncryptionKey.name} environment variable to ensure data safety and persistance.`);
}
let keychainOptions = undefined;
if (
// Using filesystem storage either by explicit configuration
// or because the keychain is not stable.
_options.secretStorage.mode !== 'keychain' &&
(_options.secretStorage.mode === 'filesystem' ||
!stableKeychain)) {
keychainOptions = {
useFilesystem: true,
encryptionKey: fileSystemEncryptionKey.value,
filePath: `${_options.dataPath}/${constants.workspace.defaultKey}/${constants.workspace.secretPath}`,
lazyInitialization: _options.lazyInitialization
};
}
const secretStorage = SecretStorage(appName, keychainOptions);
logger.debug(`Secret storage initialized using ${keychainOptions ? 'filesystem' : 'keychain'} mode`);
const addons = {
data: objectStorage,
secrets: secretStorage,
temp: tempObjectStorage
};
// To satisfy the return type
return addons;
},
interceptors: {
/**
* Before running any command, ensure that if the OS keychain is not stable,
* a valid encryption key is provided for filesystem fallback storage, warning
* the user if not.
* @param options Options provided by CLI Core.
* @param route The current command route.
* @returns The command route, or an error route if validation fails.
*/
async beforeRunning(options, route) {
const _options = _parseOptions(options.appName);
const stableKeychain = hasStableKeychain();
const forcedKeychain = _options.secretStorage.mode === 'keychain';
const autoMode = _options.secretStorage.mode === 'auto';
const fileSystemEncryptionKey = _getEnvVar(_options.secretStorage.encryptionKeyEnvVar);
if (forcedKeychain ||
(autoMode && stableKeychain) ||
fileSystemEncryptionKey.valid)
return route;
const message = `${autoMode ? 'Your system does not have a stable keychain, using filesystem secret storage' : 'No encryption key available'}. To avoid data loss set a encryption key for the filesystem secret storage by setting the ${options.logger.colors.yellowBright(_options.secretStorage.encryptionKeyEnvVar)} environment variable.`;
return {
...route,
status: 'error',
result: new Error(message)
};
},
/**
* Before the CLI Core application exits, clean up the temporary directory if configured to do so.
* @param options Options provided by CLI Core.
* @returns A promise that resolves when the cleanup is complete.
*/
async beforeEnding(options) {
const _options = _parseOptions(options.appName);
if (_options.destroyTempOnExit) {
options.logger.debug('Destroying temporary directory');
const temp = FileStorage(_options.tempPath, _options.lazyInitialization);
await temp.destroy();
}
}
}
};
}
export { VaultExtension as default };