Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@dotenvx/dotenvx

Package Overview
Dependencies
Maintainers
2
Versions
187
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@dotenvx/dotenvx - npm Package Compare versions

Comparing version 0.3.9 to 0.4.0

5

package.json
{
"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": {

7

README.md

@@ -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 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc