New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@dotenvx/dotenvx

Package Overview
Dependencies
Maintainers
2
Versions
200
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

to
0.8.0

src/cli/actions/encrypt.js

11

package.json
{
"version": "0.7.4",
"version": "0.8.0",
"name": "@dotenvx/dotenvx",

@@ -27,4 +27,13 @@ "description": "a better dotenv–from the creator of `dotenv`",

"dependencies": {
"@inquirer/prompts": "^3.3.0",
"axios": "^1.6.2",
"chalk": "^4.1.2",
"clipboardy": "^2.3.0",
"commander": "^11.1.0",
"conf": "^10.2.0",
"dotenv": "^16.3.1",
"open": "^8.4.2",
"ora": "^5.4.1",
"qrcode-terminal": "^0.12.0",
"update-notifier": "^5.1.0",
"execa": "^5.1.1",

@@ -31,0 +40,0 @@ "winston": "^3.11.0",

@@ -116,2 +116,12 @@ ![dotenvx](https://dotenvx.com/better-banner.png)

</details>
* <details><summary>Bash 🖥️</summary><br>
```sh
$ echo "HELLO=World" > .env
$ dotenvx run --quiet -- sh -c 'echo $HELLO'
World
```
</details>
* <details><summary>Frameworks ▲</summary><br>

@@ -118,0 +128,0 @@

270

src/cli/dotenvx.js
#!/usr/bin/env node
const fs = require('fs')
const updateNotifier = require('update-notifier')
const { Command } = require('commander')
const program = new Command()
// constants
const ENCODING = 'utf8'
const logger = require('./../shared/logger')

@@ -15,4 +12,9 @@ const helpers = require('./helpers')

const packageJson = require('./../shared/packageJson')
const main = require('./../lib/main')
// once a day check for any updates
const notifier = updateNotifier({ pkg: packageJson })
if (notifier.update) {
logger.warn(`Update available ${notifier.update.current} → ${notifier.update.latest} [see changelog](dotenvx.com/changelog)`)
}
// global log levels

@@ -62,115 +64,4 @@ program

.option('-o, --overload', 'override existing env variables')
.action(async function () {
const options = this.opts()
logger.debug('configuring options')
logger.debug(options)
.action(require('./actions/run'))
// load from .env.vault file
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
const filepath = helpers.resolvePath('.env.vault')
if (!fs.existsSync(filepath)) {
logger.error(`you set DOTENV_KEY but your .env.vault file is missing: ${filepath}`)
} else {
logger.verbose(`loading env from encrypted ${filepath}`)
try {
logger.debug(`reading encrypted env from ${filepath}`)
const src = fs.readFileSync(filepath, { encoding: ENCODING })
logger.debug(`parsing encrypted env from ${filepath}`)
const parsedVault = main.parse(src)
logger.debug(`decrypting encrypted env from ${filepath}`)
// handle scenario for comma separated keys - for use with key rotation
// example: DOTENV_KEY="dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenv.org/vault/.env.vault?environment=prod"
const dotenvKeys = process.env.DOTENV_KEY.split(',')
const length = dotenvKeys.length
let decrypted
for (let i = 0; i < length; i++) {
try {
// Get full dotenvKey
const dotenvKey = dotenvKeys[i].trim()
const key = helpers._parseEncryptionKeyFromDotenvKey(dotenvKey)
const ciphertext = helpers._parseCipherTextFromDotenvKeyAndParsedVault(dotenvKey, parsedVault)
// Decrypt
decrypted = main.decrypt(ciphertext, key)
break
} catch (error) {
// last key
if (i + 1 >= length) {
throw error
}
// try next key
}
}
logger.debug(decrypted)
logger.debug(`parsing decrypted env from ${filepath}`)
const parsed = main.parse(decrypted)
logger.debug(`writing decrypted env from ${filepath}`)
const result = main.write(process.env, parsed, options.overload)
logger.info(`loading env (${result.written.size}) from encrypted .env.vault`)
} catch (e) {
logger.error(e)
}
}
} else {
// convert to array if needed
let optionEnvFile = options.envFile
if (!Array.isArray(optionEnvFile)) {
optionEnvFile = [optionEnvFile]
}
const readableFilepaths = new Set()
const written = new Set()
for (const envFilepath of optionEnvFile) {
const filepath = helpers.resolvePath(envFilepath)
logger.verbose(`loading env from ${filepath}`)
try {
logger.debug(`reading env from ${filepath}`)
const src = fs.readFileSync(filepath, { encoding: ENCODING })
logger.debug(`parsing env from ${filepath}`)
const parsed = main.parse(src)
logger.debug(`writing env from ${filepath}`)
const result = main.write(process.env, parsed, options.overload)
readableFilepaths.add(envFilepath)
result.written.forEach(key => written.add(key))
} catch (e) {
logger.warn(e)
}
}
if (readableFilepaths.size > 0) {
logger.info(`loading env (${written.size}) from ${[...readableFilepaths]}`)
}
}
// Extract command and arguments after '--'
const commandIndex = process.argv.indexOf('--')
if (commandIndex === -1 || commandIndex === process.argv.length - 1) {
logger.error('missing command after [dotenvx run --]')
logger.error('')
logger.error(' get help: [dotenvx help run]')
logger.error(' or try: [dotenvx run -- npm run dev]')
process.exit(1)
} else {
const subCommand = process.argv.slice(commandIndex + 1)
await helpers.executeCommand(subCommand, process.env)
}
})
// dotenvx encrypt

@@ -181,146 +72,7 @@ program.command('encrypt')

.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)
.action(require('./actions/encrypt'))
let optionEnvFile = options.envFile
if (!Array.isArray(optionEnvFile)) {
optionEnvFile = [optionEnvFile]
}
// dotenvx hub
program.addCommand(require('./commands/hub'))
const addedKeys = new Set()
const addedVaults = new Set()
const addedEnvFilepaths = new Set()
try {
logger.verbose(`generating .env.keys from ${optionEnvFile}`)
const dotenvKeys = (main.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
addedKeys.add(key) // for info logging to user
} 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)
}
// used later in logging to user
const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
try {
logger.verbose(`generating .env.vault from ${optionEnvFile}`)
const dotenvVaults = (main.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
addedVaults.add(vault) // for info logging to user
addedEnvFilepaths.add(envFilepath) // for info logging to user
} 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)
}
if (addedEnvFilepaths.size > 0) {
logger.info(`encrypted to .env.vault (${[...addedEnvFilepaths]})`)
} else {
logger.info(`no changes (${optionEnvFile})`)
}
if (addedKeys.size > 0) {
logger.info(`${helpers.pluralize('key', addedKeys.size)} added to .env.keys (${[...addedKeys]})`)
}
if (addedVaults.size > 0) {
const DOTENV_VAULT_X = [...addedVaults][addedVaults.size - 1]
const DOTENV_KEY_X = DOTENV_VAULT_X.replace('_VAULT_', '_KEY_')
const tryKey = dotenvKeys[DOTENV_KEY_X] || '<dotenv_key_environment>'
logger.info('')
logger.info('next, try it:')
logger.info('')
logger.info(` [DOTENV_KEY='${tryKey}' dotenvx run -- your-cmd]`)
}
logger.verbose('')
logger.verbose('next:')
logger.verbose('')
logger.verbose(' 1. commit .env.vault safely to code')
logger.verbose(' 2. set DOTENV_KEY on server (or ci)')
logger.verbose(' 3. push your code')
logger.verbose('')
logger.verbose('tips:')
logger.verbose('')
logger.verbose(' * .env.keys file holds your decryption DOTENV_KEYs')
logger.verbose(' * DO NOT commit .env.keys to code')
logger.verbose(' * share .env.keys file over secure channels only')
})
program.parse(process.argv)

@@ -19,3 +19,3 @@ const run = function () {

$ dotenvx run -- node index.js
[dotenvx][info] loading env (1) from .env
[dotenvx] injecting env (1) from .env
Hello World

@@ -42,7 +42,7 @@ \`\`\`

$ dotenvx encrypt
[dotenvx][info] encrypted to .env.vault (.env,.env.production)
[dotenvx][info] keys added to .env.keys (DOTENV_KEY_PRODUCTION,DOTENV_KEY_PRODUCTION)
encrypted to .env.vault (.env,.env.production)
keys added to .env.keys (DOTENV_KEY_PRODUCTION,DOTENV_KEY_PRODUCTION)
$ DOTENV_KEY='<dotenv_key_production>' dotenvx run -- node index.js
[dotenvx][info] loading env (1) from encrypted .env.vault
[dotenvx] injecting env (1) from encrypted .env.vault
Hello production

@@ -49,0 +49,0 @@ \`\`\`

@@ -5,2 +5,3 @@ const fs = require('fs')

const crypto = require('crypto')
const { execSync } = require('child_process')
const xxhash = require('xxhashjs')

@@ -17,2 +18,6 @@

const sleep = function (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// resolve path based on current running process location

@@ -210,3 +215,32 @@ const resolvePath = function (filepath) {

const formatCode = function (str) {
const parts = []
for (let i = 0; i < str.length; i += 4) {
parts.push(str.substring(i, i + 4))
}
return parts.join('-')
}
const getRemoteOriginUrl = function () {
try {
const url = execSync('git remote get-url origin 2> /dev/null').toString().trim()
return url
} catch (_error) {
return null
}
}
const extractUsernameName = function (url) {
// Removing the protocol part and splitting by slashes and colons
// Removing the protocol part and .git suffix, then splitting by slashes and colons
const parts = url.replace(/(^\w+:|^)\/\//, '').replace(/\.git$/, '').split(/[/:]/)
// Extract the 'username/repository' part
return parts.slice(-2).join('/')
}
module.exports = {
sleep,
resolvePath,

@@ -222,4 +256,7 @@ executeCommand,

hash,
formatCode,
getRemoteOriginUrl,
extractUsernameName,
_parseEncryptionKeyFromDotenvKey,
_parseCipherTextFromDotenvKeyAndParsedVault
}

@@ -24,8 +24,8 @@ const logger = require('./../shared/logger')

const write = function (processEnv = {}, parsed = {}, overload = false) {
const inject = function (processEnv = {}, parsed = {}, overload = false) {
if (typeof parsed !== 'object') {
throw new Error('OBJECT_REQUIRED: Please check the parsed argument being passed to write')
throw new Error('OBJECT_REQUIRED: Please check the parsed argument being passed to inject')
}
const written = new Set()
const injected = new Set()
const preExisting = new Set()

@@ -38,3 +38,3 @@

processEnv[key] = parsed[key]
written.add(key)
injected.add(key)

@@ -46,8 +46,8 @@ logger.verbose(`${key} set`)

logger.verbose(`${key} pre-exists`)
logger.debug(`${key} pre-exists as ${processEnv[key]}`)
logger.verbose(`${key} pre-exists (protip: use --overload to override)`)
logger.debug(`${key} pre-exists as ${processEnv[key]} (protip: use --overload to override)`)
}
} else {
processEnv[key] = parsed[key]
written.add(key)
injected.add(key)

@@ -60,3 +60,3 @@ logger.verbose(`${key} set`)

return {
written,
injected,
preExisting

@@ -71,3 +71,3 @@ }

parse,
write
inject
}
const winston = require('winston')
const chalk = require('chalk')

@@ -10,6 +11,56 @@ const printf = winston.format.printf

const levels = {
error: 0,
warn: 1,
success: 2,
successv: 2,
info: 2,
help: 2,
help2: 2,
blank: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
}
const error = chalk.bold.red
const warn = chalk.keyword('orangered')
const success = chalk.keyword('green')
const successv = chalk.keyword('olive') // yellow-ish tint that 'looks' like dotenv
const help = chalk.keyword('blue')
const help2 = chalk.keyword('gray')
const http = chalk.keyword('green')
const verbose = chalk.keyword('plum')
const debug = chalk.keyword('plum')
const dotenvxFormat = printf(({ level, message, label, timestamp }) => {
const formattedMessage = typeof message === 'object' ? JSON.stringify(message) : message
return `[dotenvx@${packageJson.version}][${level.toLowerCase()}] ${formattedMessage}`
switch (level.toLowerCase()) {
case 'error':
return error(formattedMessage)
case 'warn':
return warn(formattedMessage)
case 'success':
return success(formattedMessage)
case 'successv': // success with 'version'
return successv(`[dotenvx@${packageJson.version}] ${formattedMessage}`)
case 'info':
return formattedMessage
case 'help':
return help(formattedMessage)
case 'help2':
return help2(formattedMessage)
case 'http':
return http(formattedMessage)
case 'verbose':
return verbose(formattedMessage)
case 'debug':
return debug(formattedMessage)
case 'blank': // custom
return formattedMessage
default: // handle uncaught
return formattedMessage
}
})

@@ -19,2 +70,3 @@

level: 'info',
levels,
format: combine(

@@ -21,0 +73,0 @@ dotenvxFormat