@dotenvx/dotenvx
Advanced tools
Comparing version 0.3.9 to 0.4.0
{ | ||
"version": "0.3.9", | ||
"version": "0.4.0", | ||
"name": "@dotenvx/dotenvx", | ||
@@ -29,3 +29,4 @@ "description": "a better dotenv–from the creator of `dotenv`", | ||
"dotenv": "^16.3.1", | ||
"winston": "^3.11.0" | ||
"winston": "^3.11.0", | ||
"xxhashjs": "^0.2.2" | ||
}, | ||
@@ -32,0 +33,0 @@ "devDependencies": { |
@@ -160,4 +160,9 @@ ![dotenvx](https://dotenvx.com/better-banner.png) | ||
WIP | ||
``` | ||
dotenvx encrypt | ||
``` | ||
> This will encrypt your `.env` file to a `.env.vault` file. Commit your `.env.vault` file safely to code. | ||
> This will also generate a `.env.keys` file. Do NOT commit this file to code. Keep your `.env.keys` secret. 🤫 | ||
| ||
@@ -164,0 +169,0 @@ |
@@ -5,2 +5,3 @@ #!/usr/bin/env node | ||
const { Command } = require('commander') | ||
const dotenv = require('dotenv') | ||
const program = new Command() | ||
@@ -27,3 +28,3 @@ | ||
logger.level = options.logLevel | ||
logger.debug(`Setting log level to ${options.logLevel}`) | ||
logger.debug(`setting log level to ${options.logLevel}`) | ||
} | ||
@@ -53,10 +54,10 @@ | ||
// dotenvx run -- node index.js | ||
program.command('run') | ||
.description('inject env variables into your application process') | ||
.option('-f, --env-file <paths...>', 'path to your env file', '.env') | ||
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)', '.env') | ||
.option('-o, --overload', 'override existing env variables') | ||
.action(function () { | ||
// injecting 1 environment variable from ${options.envFile} | ||
const options = this.opts() | ||
logger.debug('Configuring options') | ||
logger.debug('configuring options') | ||
logger.debug(options) | ||
@@ -72,3 +73,3 @@ | ||
const readableFilepaths = new Set() | ||
const populated = new Set() | ||
const written = new Set() | ||
@@ -78,16 +79,16 @@ for (const envFilepath of optionEnvFile) { | ||
logger.verbose(`Loading env from ${filepath}`) | ||
logger.verbose(`injecting env from ${filepath}`) | ||
try { | ||
logger.debug(`Reading env from ${filepath}`) | ||
logger.debug(`reading env from ${filepath}`) | ||
const src = fs.readFileSync(filepath, { encoding: ENCODING }) | ||
logger.debug(`Parsing env from ${filepath}`) | ||
logger.debug(`parsing env from ${filepath}`) | ||
const parsed = main.parse(src) | ||
logger.debug(`Populating env from ${filepath}`) | ||
const result = main.populate(process.env, parsed, options.overload) | ||
logger.debug(`writing env from ${filepath}`) | ||
const result = main.write(process.env, parsed, options.overload) | ||
readableFilepaths.add(envFilepath) | ||
result.populated.forEach(key => populated.add(key)) | ||
result.written.forEach(key => written.add(key)) | ||
} catch (e) { | ||
@@ -99,3 +100,3 @@ logger.warn(e) | ||
if (readableFilepaths.size > 0) { | ||
logger.info(`Injecting ${populated.size} environment variables from ${[...readableFilepaths]}`) | ||
logger.info(`injecting ${written.size} environment ${helpers.pluralize('variable', written.size)} from ${[...readableFilepaths]}`) | ||
} | ||
@@ -106,4 +107,3 @@ | ||
if (commandIndex === -1 || commandIndex === process.argv.length - 1) { | ||
logger.error('At least one argument is required after the run command, received 0.') | ||
logger.error('Exiting') | ||
logger.error('at least one argument is required after the run command, received 0.') | ||
process.exit(1) | ||
@@ -117,2 +117,110 @@ } else { | ||
// dotenvx encrypt | ||
program.command('encrypt') | ||
.description('encrypt .env.* to .env.vault') | ||
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)', helpers.findEnvFiles('./')) | ||
.action(function () { | ||
const options = this.opts() | ||
logger.debug('configuring options') | ||
logger.debug(options) | ||
let optionEnvFile = options.envFile | ||
if (!Array.isArray(optionEnvFile)) { | ||
optionEnvFile = [optionEnvFile] | ||
} | ||
try { | ||
logger.verbose(`generating .env.keys from ${optionEnvFile}`) | ||
const dotenvKeys = (dotenv.configDotenv({ path: '.env.keys' }).parsed || {}) | ||
for (const envFilepath of optionEnvFile) { | ||
const filepath = helpers.resolvePath(envFilepath) | ||
if (!fs.existsSync(filepath)) { | ||
throw new Error(`file does not exist: ${filepath}`) | ||
} | ||
const environment = helpers.guessEnvironment(filepath) | ||
const key = `DOTENV_KEY_${environment.toUpperCase()}` | ||
let value = dotenvKeys[key] | ||
// first time seeing new DOTENV_KEY_${environment} | ||
if (!value || value.length === 0) { | ||
logger.verbose(`generating ${key}`) | ||
value = helpers.generateDotenvKey(environment) | ||
logger.debug(`generating ${key} as ${value}`) | ||
dotenvKeys[key] = value | ||
} else { | ||
logger.verbose(`existing ${key}`) | ||
logger.debug(`existing ${key} as ${value}`) | ||
} | ||
} | ||
let keysData = `#/!!!!!!!!!!!!!!!!!!!.env.keys!!!!!!!!!!!!!!!!!!!!!!/ | ||
#/ DOTENV_KEYs. DO NOT commit to source control / | ||
#/ [how it works](https://dotenv.org/env-keys) / | ||
#/--------------------------------------------------/\n` | ||
for (const key in dotenvKeys) { | ||
const value = dotenvKeys[key] | ||
keysData += `${key}="${value}"\n` | ||
} | ||
fs.writeFileSync('.env.keys', keysData) | ||
} catch (e) { | ||
logger.error(e) | ||
process.exit(1) | ||
} | ||
try { | ||
logger.verbose(`generating .env.vault from ${optionEnvFile}`) | ||
const dotenvKeys = (dotenv.configDotenv({ path: '.env.keys' }).parsed || {}) | ||
const dotenvVaults = (dotenv.configDotenv({ path: '.env.vault' }).parsed || {}) | ||
for (const envFilepath of optionEnvFile) { | ||
const filepath = helpers.resolvePath(envFilepath) | ||
const environment = helpers.guessEnvironment(filepath) | ||
const vault = `DOTENV_VAULT_${environment.toUpperCase()}` | ||
let ciphertext = dotenvVaults[vault] | ||
const dotenvKey = dotenvKeys[`DOTENV_KEY_${environment.toUpperCase()}`] | ||
if (!ciphertext || ciphertext.length === 0 || helpers.changed(ciphertext, dotenvKey, filepath, ENCODING)) { | ||
logger.verbose(`encrypting ${vault}`) | ||
ciphertext = helpers.encryptFile(filepath, dotenvKey, ENCODING) | ||
logger.verbose(`encrypting ${vault} as ${ciphertext}`) | ||
dotenvVaults[vault] = ciphertext | ||
} else { | ||
logger.verbose(`existing ${vault}`) | ||
logger.debug(`existing ${vault} as ${ciphertext}`) | ||
} | ||
} | ||
let vaultData = `#/-------------------.env.vault---------------------/ | ||
#/ cloud-agnostic vaulting standard / | ||
#/ [how it works](https://dotenv.org/env-vault) / | ||
#/--------------------------------------------------/\n\n` | ||
for (const vault in dotenvVaults) { | ||
const value = dotenvVaults[vault] | ||
const environment = vault.replace('DOTENV_VAULT_', '').toLowerCase() | ||
vaultData += `# ${environment}\n` | ||
vaultData += `${vault}="${value}"\n\n` | ||
} | ||
fs.writeFileSync('.env.vault', vaultData) | ||
} catch (e) { | ||
logger.error(e) | ||
process.exit(1) | ||
} | ||
logger.info(`encrypted ${optionEnvFile} to .env.vault`) | ||
// logger.info(`encrypting`) | ||
}) | ||
program.parse(process.argv) |
@@ -0,4 +1,12 @@ | ||
const fs = require('fs') | ||
const path = require('path') | ||
const crypto = require('crypto') | ||
const { spawn } = require('child_process') | ||
const xxhash = require('xxhashjs') | ||
const XXHASH_SEED = 0xABCD | ||
const main = require('./../lib/main') | ||
const RESERVED_ENV_FILES = ['.env.vault', '.env.projects', '.env.keys', '.env.me', '.env.x'] | ||
// resolve path based on current running process location | ||
@@ -25,5 +33,116 @@ const resolvePath = function (filepath) { | ||
const pluralize = function (word, count) { | ||
// simple pluralization: add 's' at the end | ||
if (count === 0 || count > 1) { | ||
return word + 's' | ||
} else { | ||
return word | ||
} | ||
} | ||
const findEnvFiles = function (directory) { | ||
const files = fs.readdirSync(directory) | ||
const envFiles = files.filter(file => | ||
file.startsWith('.env') && | ||
!file.endsWith('.previous') && | ||
!RESERVED_ENV_FILES.includes(file) | ||
) | ||
return envFiles | ||
} | ||
const guessEnvironment = function (file) { | ||
const splitFile = file.split('.') | ||
const possibleEnvironment = splitFile[2] // ['', 'env', environment'] | ||
if (!possibleEnvironment || possibleEnvironment.length === 0) { | ||
return 'development' | ||
} | ||
return possibleEnvironment | ||
} | ||
const generateDotenvKey = function (environment) { | ||
const rand = crypto.randomBytes(32).toString('hex') | ||
return `dotenv://:key_${rand}@dotenvx.com/vault/.env.vault?environment=${environment.toLowerCase()}` | ||
} | ||
const encryptFile = function (filepath, dotenvKey, encoding) { | ||
const key = this._parseEncryptionKeyFromDotenvKey(dotenvKey) | ||
const message = fs.readFileSync(filepath, encoding) | ||
const ciphertext = this.encrypt(key, message) | ||
return ciphertext | ||
} | ||
const encrypt = function (key, message) { | ||
// set up nonce | ||
const nonce = this._generateNonce() | ||
// set up cipher | ||
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce) | ||
// generate ciphertext | ||
let ciphertext = '' | ||
ciphertext += cipher.update(message, 'utf8', 'hex') | ||
ciphertext += cipher.final('hex') | ||
ciphertext += cipher.getAuthTag().toString('hex') | ||
// prepend nonce | ||
ciphertext = nonce.toString('hex') + ciphertext | ||
// base64 encode output | ||
return Buffer.from(ciphertext, 'hex').toString('base64') | ||
} | ||
const changed = function (ciphertext, dotenvKey, filepath, encoding) { | ||
const key = this._parseEncryptionKeyFromDotenvKey(dotenvKey) | ||
const decrypted = main.decrypt(ciphertext, key) | ||
const raw = fs.readFileSync(filepath, encoding) | ||
return this.hash(decrypted) !== this.hash(raw) | ||
} | ||
const hash = function (str) { | ||
return xxhash.h32(str, XXHASH_SEED).toString(16) | ||
} | ||
const _parseEncryptionKeyFromDotenvKey = function (dotenvKey) { | ||
// Parse DOTENV_KEY. Format is a URI | ||
const uri = new URL(dotenvKey) | ||
// Get decrypt key | ||
const key = uri.password | ||
if (!key) { | ||
throw new Error('INVALID_DOTENV_KEY: Missing key part') | ||
} | ||
return Buffer.from(key.slice(-64), 'hex') | ||
} | ||
const _generateNonce = function () { | ||
return crypto.randomBytes(this._nonceBytes()) | ||
} | ||
const _nonceBytes = function () { | ||
return 12 | ||
} | ||
module.exports = { | ||
resolvePath, | ||
executeCommand | ||
executeCommand, | ||
pluralize, | ||
findEnvFiles, | ||
guessEnvironment, | ||
generateDotenvKey, | ||
encryptFile, | ||
encrypt, | ||
changed, | ||
hash, | ||
_parseEncryptionKeyFromDotenvKey, | ||
_generateNonce, | ||
_nonceBytes | ||
} |
@@ -8,2 +8,6 @@ const logger = require('./../shared/logger') | ||
const decrypt = function (encrypted, keyStr) { | ||
return dotenv.decrypt(encrypted, keyStr) | ||
} | ||
const parse = function (src) { | ||
@@ -17,8 +21,8 @@ const result = dotenv.parse(src) | ||
const populate = function (processEnv = {}, parsed = {}, overload = false) { | ||
const write = function (processEnv = {}, parsed = {}, overload = false) { | ||
if (typeof parsed !== 'object') { | ||
throw new Error('OBJECT_REQUIRED: Please check the parsed argument being passed to populate') | ||
throw new Error('OBJECT_REQUIRED: Please check the parsed argument being passed to write') | ||
} | ||
const populated = new Set() | ||
const written = new Set() | ||
const preExisting = new Set() | ||
@@ -31,3 +35,3 @@ | ||
processEnv[key] = parsed[key] | ||
populated.add(key) | ||
written.add(key) | ||
@@ -44,3 +48,3 @@ logger.verbose(`${key} set`) | ||
processEnv[key] = parsed[key] | ||
populated.add(key) | ||
written.add(key) | ||
@@ -53,3 +57,3 @@ logger.verbose(`${key} set`) | ||
return { | ||
populated, | ||
written, | ||
preExisting | ||
@@ -61,4 +65,5 @@ } | ||
config, | ||
decrypt, | ||
parse, | ||
populate | ||
write | ||
} |
@@ -10,6 +10,10 @@ const winston = require('winston') | ||
function pad (word) { | ||
return word.padEnd(9, ' ') | ||
} | ||
const dotenvxFormat = printf(({ level, message, label, timestamp }) => { | ||
const formattedMessage = typeof message === 'object' ? JSON.stringify(message) : message | ||
return `[dotenvx@${packageJson.version}][${level.toUpperCase()}] ${formattedMessage}` | ||
return `[dotenvx@${packageJson.version}]${pad(`[${level.toUpperCase()}]`)} ${formattedMessage}` | ||
}) | ||
@@ -16,0 +20,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
19674
366
308
4
6
+ Addedxxhashjs@^0.2.2
+ Addedcuint@0.2.2(transitive)
+ Addedxxhashjs@0.2.2(transitive)