├── .github ├── actions │ └── commit-binary-to-github │ │ └── action.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .shellspec ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── esbuild.js ├── install.sh ├── package-lock.json ├── package.json ├── spec ├── install_spec.sh ├── spec_helper.sh └── tmp │ └── .gitkeep ├── src ├── cli │ ├── actions │ │ ├── decrypt.js │ │ ├── encrypt.js │ │ ├── ext │ │ │ ├── genexample.js │ │ │ ├── gitignore.js │ │ │ ├── prebuild.js │ │ │ ├── precommit.js │ │ │ └── scan.js │ │ ├── get.js │ │ ├── keypair.js │ │ ├── ls.js │ │ ├── rotate.js │ │ ├── run.js │ │ └── set.js │ ├── commands │ │ └── ext.js │ ├── dotenvx.js │ └── examples.js ├── lib │ ├── config.d.ts │ ├── config.js │ ├── helpers │ │ ├── append.js │ │ ├── arrayToTree.js │ │ ├── buildEnvs.js │ │ ├── catchAndLog.js │ │ ├── chomp.js │ │ ├── colorDepth.js │ │ ├── conventions.js │ │ ├── decrypt.js │ │ ├── decryptKeyValue.js │ │ ├── deprecationNotice.js │ │ ├── detectEncoding.js │ │ ├── determineEnvs.js │ │ ├── dotenvOptionPaths.js │ │ ├── dotenvParse.js │ │ ├── dotenvPrivateKeyNames.js │ │ ├── encrypt.js │ │ ├── encryptValue.js │ │ ├── errors.js │ │ ├── escape.js │ │ ├── escapeDollarSigns.js │ │ ├── escapeForRegex.js │ │ ├── evalKeyValue.js │ │ ├── execute.js │ │ ├── executeCommand.js │ │ ├── executeDynamic.js │ │ ├── executeExtension.js │ │ ├── findEnvFiles.js │ │ ├── findPrivateKey.js │ │ ├── findPublicKey.js │ │ ├── fsx.js │ │ ├── getCommanderVersion.js │ │ ├── guessEnvironment.js │ │ ├── guessPrivateKeyFilename.js │ │ ├── guessPrivateKeyName.js │ │ ├── guessPublicKeyName.js │ │ ├── installPrecommitHook.js │ │ ├── isEncrypted.js │ │ ├── isFullyEncrypted.js │ │ ├── isIgnoringDotenvKeys.js │ │ ├── isPublicKey.js │ │ ├── keypair.js │ │ ├── packageJson.js │ │ ├── parse.js │ │ ├── parseEncryptionKeyFromDotenvKey.js │ │ ├── parseEnvironmentFromDotenvKey.js │ │ ├── pluralize.js │ │ ├── proKeypair.js │ │ ├── quotes.js │ │ ├── removeDynamicHelpSection.js │ │ ├── removeOptionsHelpParts.js │ │ ├── replace.js │ │ ├── resolveEscapeSequences.js │ │ ├── resolveHome.js │ │ ├── sleep.js │ │ ├── smartDotenvPrivateKey.js │ │ ├── smartDotenvPublicKey.js │ │ └── truncate.js │ ├── main.d.ts │ ├── main.js │ └── services │ │ ├── decrypt.js │ │ ├── encrypt.js │ │ ├── genexample.js │ │ ├── get.js │ │ ├── keypair.js │ │ ├── ls.js │ │ ├── prebuild.js │ │ ├── precommit.js │ │ ├── rotate.js │ │ ├── run.js │ │ └── sets.js └── shared │ ├── colors.js │ └── logger.js └── tests ├── .env ├── .env.eval ├── .env.expand ├── .env.export ├── .env.latin1 ├── .env.local ├── .env.multiline ├── .env.utf16le ├── .env.vault ├── cli ├── actions │ ├── decrypt.test.js │ ├── encrypt.test.js │ ├── ext │ │ ├── genexample.test.js │ │ ├── gitignore.test.js │ │ ├── prebuild.test.js │ │ ├── precommit.test.js │ │ └── scan.test.js │ ├── get.test.js │ ├── keypair.test.js │ ├── ls.test.js │ ├── rotate.test.js │ ├── run.test.js │ └── set.test.js └── examples.test.js ├── e2e ├── decrypt.test.js ├── encrypt.test.js ├── ext.test.js ├── get.test.js ├── run.test.js └── version.test.js ├── lib ├── config-expand.test.js ├── config-vault.test.js ├── config.test.js ├── helpers │ ├── append.test.js │ ├── arrayToTree.test.js │ ├── colorDepth.test.js │ ├── conventions.test.js │ ├── decrypt.test.js │ ├── decryptKeyValue.test.js │ ├── detectEncoding.test.js │ ├── encryptValue.test.js │ ├── errors.test.js │ ├── executeCommand.test.js │ ├── executeDynamic.test.js │ ├── executeExtension.test.js │ ├── findEnvFiles.test.js │ ├── findPrivateKey.test.js │ ├── fsx.test.js │ ├── guessEnvironment.test.js │ ├── guessPrivateKeyFilename.test.js │ ├── guessPrivateKeyName.test.js │ ├── guessPublicKeyName.test.js │ ├── installPrecommitHook.test.js │ ├── isEncrypted.test.js │ ├── isFullyEncrypted.test.js │ ├── isIgnoringDotenvKeys.test.js │ ├── isPublicKey.test.js │ ├── keypair.test.js │ ├── parse.test.js │ ├── parseEncryptionKeyFromDotenvKey.test.js │ ├── parseEnvironmentFromDotenvKey.test.js │ ├── pluralize.test.js │ ├── proKeypair.test.js │ ├── removeDynamicHelpSection.test.js │ ├── removeOptionsHelpParts.test.js │ ├── replace.test.js │ ├── smartDotenvPrivateKey.test.js │ ├── smartDotenvPublicKey.test.js │ └── truncate.test.js ├── main.test.js ├── parse-multiline.test.js ├── parse.test.js └── services │ ├── decrypt.test.js │ ├── encrypt.test.js │ ├── genexample.test.js │ ├── get.test.js │ ├── keypair.test.js │ ├── ls.test.js │ ├── prebuild.test.js │ ├── precommit.test.js │ ├── rotate.test.js │ ├── run.test.js │ └── sets.test.js ├── monorepo ├── .env.keys └── apps │ ├── app1 │ ├── .env │ └── .env.production │ ├── backend │ ├── .env │ ├── .env.example │ ├── .env.keys │ ├── .env.previous │ ├── .env.untracked │ └── .env.vault │ ├── encrypted │ ├── .env │ ├── .env.keys │ ├── secrets.ci.txt │ └── secrets.txt │ ├── frontend │ └── .env │ ├── multiline │ ├── .env │ └── .env.crlf │ ├── multiple │ ├── .env │ ├── .env.keys │ └── .env.production │ ├── shebang │ └── .env │ └── unencrypted │ └── .env ├── multiline.txt └── shared ├── colors.test.js └── logger.test.js /.github/actions/commit-binary-to-github/action.yml: -------------------------------------------------------------------------------- 1 | name: commit-binary-to-github 2 | description: commit binary to github repo - which triggers to npm publish 3 | inputs: 4 | version: 5 | description: 'choose version' 6 | required: true 7 | default: 'v0.30.0' 8 | platform: 9 | description: 'choose platform' 10 | required: true 11 | default: 'darwin-arm64' 12 | 13 | runs: 14 | using: "composite" 15 | steps: 16 | - name: extract version 17 | shell: bash 18 | run: | 19 | VERSION=${{inputs.version}} 20 | CLEAN_VERSION=${VERSION#v} 21 | echo "VERSION=$VERSION" >> $GITHUB_ENV 22 | echo "CLEAN_VERSION=$CLEAN_VERSION" >> $GITHUB_ENV 23 | - name: node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 16.x 27 | 28 | - name: download and extract tarball 29 | shell: bash 30 | run: | 31 | curl -L -o tarball.tar.gz https://github.com/dotenvx/dotenvx/releases/download/${{ env.VERSION }}/dotenvx-${{ env.CLEAN_VERSION }}-${{ inputs.platform }}.tar.gz 32 | mkdir -p output 33 | tar -xzvf tarball.tar.gz -C output --strip-components=1 34 | 35 | - name: checkout target repo 36 | uses: actions/checkout@v4 37 | with: 38 | repository: "dotenvx/dotenvx-${{ inputs.platform }}" 39 | token: ${{ env.PERSONAL_ACCESS_TOKEN }} 40 | path: "dotenvx-${{ inputs.platform }}" 41 | 42 | - name: copy dotenvx binary to repo 43 | shell: bash 44 | run: | 45 | mkdir -p dotenvx-${{ inputs.platform }} 46 | cp -r output/* dotenvx-${{ inputs.platform }}/ 47 | 48 | - name: set version, commit, and push 49 | shell: bash 50 | run: | 51 | cd dotenvx-${{ inputs.platform }} 52 | npm version ${{ env.CLEAN_VERSION }} --no-git-tag-version 53 | git config --global user.name 'motdotenv' 54 | git config --global user.email 'mot@dotenv.org' 55 | git add . 56 | git commit -m "${{ env.VERSION }}" 57 | git tag "${{ env.VERSION }}" 58 | git push origin HEAD --tags 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .tap 4 | coverage/ 5 | node_modules/ 6 | build/* 7 | bin/* 8 | dist/* 9 | .env* 10 | !.env.vault 11 | tests/tmp 12 | .vscode 13 | 14 | # for test purposes 15 | !tests/.env 16 | !tests/.env.expand 17 | !tests/.env.export 18 | !tests/.env.eval 19 | !tests/.env.local 20 | !tests/.env.multiline 21 | !tests/.env.vault 22 | !tests/.env.utf16le 23 | !tests/.env.latin1 24 | !tests/monorepo/**/.env* 25 | spec/tmp/* 26 | !spec/tmp/.gitkeep 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .tap 4 | coverage/ 5 | node_modules/ 6 | dist/* 7 | .env* 8 | !.env.vault 9 | tests/ 10 | spec/ 11 | .shellspec 12 | -------------------------------------------------------------------------------- /.shellspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | 3 | ## Default kcov (coverage) options 4 | # --kcov-options "--include-path=. --path-strip-level=1" 5 | # --kcov-options "--include-pattern=.sh" 6 | # --kcov-options "--exclude-pattern=/.shellspec,/spec/,/coverage/,/report/" 7 | 8 | ## Example: Include script "myprog" with no extension 9 | # --kcov-options "--include-pattern=.sh,myprog" 10 | 11 | ## Example: Only specified files/directories 12 | # --kcov-options "--include-pattern=myprog,/lib/" 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use a base image 2 | FROM ubuntu:latest 3 | 4 | # Set work directory 5 | WORKDIR /app 6 | 7 | # Set environment variables for the architecture and OS (change if necessary) 8 | ENV OS=linux 9 | 10 | # Install dependencies 11 | RUN apt-get update && apt-get install -y curl 12 | 13 | # Extract architecture from TARGETPLATFORM environment variable and fetch dotenvx binary 14 | ARG TARGETPLATFORM 15 | RUN ARCH=$(echo ${TARGETPLATFORM} | cut -f2 -d '/') && \ 16 | echo "Architecture: ${ARCH}" && \ 17 | curl -L https://github.com/dotenvx/dotenvx/releases/latest/download/dotenvx-${OS}-${ARCH}.tar.gz | tar -xz -C /usr/local/bin 18 | 19 | # Make the binary executable 20 | RUN chmod +x /usr/local/bin/dotenvx 21 | 22 | # Set the entry point to the dotenvx command (optional) 23 | ENTRYPOINT ["/usr/local/bin/dotenvx"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Scott Motte 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Please report any security vulnerabilities to security@dotenvx.com. 2 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild') 2 | const { stat, writeFile, rm, mkdir } = require('fs/promises') 3 | const pkgJson = require('./package.json') 4 | 5 | const outputDir = 'build' 6 | 7 | function cleanPkgJson (json) { 8 | delete json.devDependencies 9 | delete json.optionalDependencies 10 | delete json.dependencies 11 | delete json.workspaces 12 | return json 13 | } 14 | 15 | async function emptyDir (dir) { 16 | try { 17 | await rm(dir, { recursive: true }) 18 | } catch (err) { 19 | if (err.code !== 'ENOENT') { 20 | throw err 21 | } 22 | } 23 | await mkdir(dir) 24 | } 25 | 26 | async function printSize (fileName) { 27 | const stats = await stat(fileName) 28 | 29 | // print size in MB 30 | console.log(`Bundle size: ${Math.round(stats.size / 10000) / 100}MB\n\n`) 31 | } 32 | 33 | async function main () { 34 | const start = Date.now() 35 | // clean build folder 36 | await emptyDir(outputDir) 37 | 38 | const outfile = `${outputDir}/index.js` 39 | 40 | /** @type { import('esbuild').BuildOptions } */ 41 | const config = { 42 | entryPoints: [pkgJson.bin.dotenvx], 43 | bundle: true, 44 | platform: 'node', 45 | target: 'node18', 46 | sourcemap: true, 47 | outfile, 48 | // suppress direct-eval warning 49 | logOverride: { 50 | 'direct-eval': 'silent' 51 | } 52 | } 53 | 54 | await esbuild.build(config) 55 | 56 | console.log(`Build took ${Date.now() - start}ms`) 57 | await printSize(outfile) 58 | 59 | if (process.argv.includes('--minify')) { 60 | // minify the file 61 | await esbuild.build({ 62 | ...config, 63 | entryPoints: [outfile], 64 | minify: true, 65 | keepNames: true, 66 | allowOverwrite: true, 67 | outfile 68 | }) 69 | 70 | console.log(`Minify took ${Date.now() - start}ms`) 71 | await printSize(outfile) 72 | } 73 | 74 | // create main patched package.json 75 | cleanPkgJson(pkgJson) 76 | 77 | pkgJson.scripts = { 78 | start: 'node index.js' 79 | } 80 | 81 | pkgJson.bin = 'index.js' 82 | pkgJson.pkg = { 83 | assets: [ 84 | '*.map' 85 | ] 86 | } 87 | 88 | await writeFile( 89 | `${outputDir}/package.json`, 90 | JSON.stringify(pkgJson, null, 2) 91 | ) 92 | } 93 | 94 | main().catch(err => { 95 | console.error(err) 96 | process.exit(1) 97 | }) 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.46.0", 3 | "name": "@dotenvx/dotenvx", 4 | "description": "a better dotenv–from the creator of `dotenv`", 5 | "author": "@motdotla", 6 | "keywords": [ 7 | "dotenv", 8 | "env" 9 | ], 10 | "homepage": "https://github.com/dotenvx/dotenvx", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/dotenvx/dotenvx.git" 14 | }, 15 | "license": "BSD-3-Clause", 16 | "files": [ 17 | "src/**/*", 18 | "CHANGELOG.md" 19 | ], 20 | "main": "src/lib/main.js", 21 | "types": "src/lib/main.d.ts", 22 | "exports": { 23 | ".": { 24 | "types": "./src/lib/main.d.ts", 25 | "require": "./src/lib/main.js", 26 | "default": "./src/lib/main.js" 27 | }, 28 | "./config": "./src/lib/config.js", 29 | "./config.js": "./src/lib/config.js", 30 | "./package.json": "./package.json" 31 | }, 32 | "bin": { 33 | "dotenvx": "./src/cli/dotenvx.js" 34 | }, 35 | "scripts": { 36 | "standard": "standard", 37 | "standard:fix": "standard --fix", 38 | "test": "tap run --allow-empty-coverage --disable-coverage --timeout=60000", 39 | "test-coverage": "tap run --show-full-coverage --timeout=60000", 40 | "testshell": "bash shellspec", 41 | "prerelease": "npm test && npm run testshell", 42 | "release": "standard-version" 43 | }, 44 | "funding": "https://dotenvx.com", 45 | "dependencies": { 46 | "commander": "^11.1.0", 47 | "dotenv": "^16.4.5", 48 | "eciesjs": "^0.4.10", 49 | "execa": "^5.1.1", 50 | "fdir": "^6.2.0", 51 | "ignore": "^5.3.0", 52 | "object-treeify": "1.1.33", 53 | "picomatch": "^4.0.2", 54 | "which": "^4.0.0" 55 | }, 56 | "devDependencies": { 57 | "@yao-pkg/pkg": "^5.14.2", 58 | "capture-console": "^1.0.2", 59 | "esbuild": "^0.24.0", 60 | "proxyquire": "^2.1.3", 61 | "sinon": "^14.0.1", 62 | "standard": "^17.1.0", 63 | "standard-version": "^9.5.0", 64 | "tap": "^21.0.1" 65 | }, 66 | "publishConfig": { 67 | "access": "public", 68 | "provenance": true 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /spec/spec_helper.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | 3 | # Defining variables and functions here will affect all specfiles. 4 | # Change shell options inside a function may cause different behavior, 5 | # so it is better to set them here. 6 | # set -eu 7 | 8 | # This callback function will be invoked only once before loading specfiles. 9 | spec_helper_precheck() { 10 | # Available functions: info, warn, error, abort, setenv, unsetenv 11 | # Available variables: VERSION, SHELL_TYPE, SHELL_VERSION 12 | : minimum_version "0.28.1" 13 | } 14 | 15 | # This callback function will be invoked after a specfile has been loaded. 16 | spec_helper_loaded() { 17 | : 18 | } 19 | 20 | # This callback function will be invoked after core modules has been loaded. 21 | spec_helper_configure() { 22 | # Available functions: import, before_each, after_each, before_all, after_all 23 | : import 'support/custom_matcher' 24 | } 25 | -------------------------------------------------------------------------------- /spec/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotenvx/dotenvx/49a78cacab5fa4b758b901f1ac739cccf2bf4562/spec/tmp/.gitkeep -------------------------------------------------------------------------------- /src/cli/actions/decrypt.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./../../lib/helpers/fsx') 2 | const { logger } = require('./../../shared/logger') 3 | 4 | const Decrypt = require('./../../lib/services/decrypt') 5 | 6 | const catchAndLog = require('../../lib/helpers/catchAndLog') 7 | 8 | function decrypt () { 9 | const options = this.opts() 10 | logger.debug(`options: ${JSON.stringify(options)}`) 11 | 12 | const envs = this.envs 13 | 14 | let errorCount = 0 15 | 16 | // stdout - should not have a try so that exit codes can surface to stdout 17 | if (options.stdout) { 18 | const { 19 | processedEnvs 20 | } = new Decrypt(envs, options.key, options.excludeKey, options.envKeysFile).run() 21 | 22 | for (const processedEnv of processedEnvs) { 23 | if (processedEnv.error) { 24 | errorCount += 1 25 | logger.error(processedEnv.error.message) 26 | if (processedEnv.error.help) { 27 | logger.error(processedEnv.error.help) 28 | } 29 | } else { 30 | console.log(processedEnv.envSrc) 31 | } 32 | } 33 | 34 | if (errorCount > 0) { 35 | process.exit(1) 36 | } else { 37 | process.exit(0) // exit early 38 | } 39 | } else { 40 | try { 41 | const { 42 | processedEnvs, 43 | changedFilepaths, 44 | unchangedFilepaths 45 | } = new Decrypt(envs, options.key, options.excludeKey, options.envKeysFile).run() 46 | 47 | for (const processedEnv of processedEnvs) { 48 | logger.verbose(`decrypting ${processedEnv.envFilepath} (${processedEnv.filepath})`) 49 | 50 | if (processedEnv.error) { 51 | errorCount += 1 52 | 53 | if (processedEnv.error.code === 'MISSING_ENV_FILE') { 54 | logger.error(processedEnv.error.message) 55 | logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx decrypt]`) 56 | } else { 57 | logger.error(processedEnv.error.message) 58 | if (processedEnv.error.help) { 59 | logger.error(processedEnv.error.help) 60 | } 61 | } 62 | } else if (processedEnv.changed) { 63 | fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc) 64 | 65 | logger.verbose(`decrypted ${processedEnv.envFilepath} (${processedEnv.filepath})`) 66 | } else { 67 | logger.verbose(`no changes ${processedEnv.envFilepath} (${processedEnv.filepath})`) 68 | } 69 | } 70 | 71 | if (changedFilepaths.length > 0) { 72 | logger.success(`✔ decrypted (${changedFilepaths.join(',')})`) 73 | } else if (unchangedFilepaths.length > 0) { 74 | logger.info(`no changes (${unchangedFilepaths})`) 75 | } else { 76 | // do nothing - scenario when no .env files found 77 | } 78 | 79 | if (errorCount > 0) { 80 | process.exit(1) 81 | } 82 | } catch (error) { 83 | catchAndLog(error) 84 | process.exit(1) 85 | } 86 | } 87 | } 88 | 89 | module.exports = decrypt 90 | -------------------------------------------------------------------------------- /src/cli/actions/encrypt.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./../../lib/helpers/fsx') 2 | const { logger } = require('./../../shared/logger') 3 | 4 | const Encrypt = require('./../../lib/services/encrypt') 5 | 6 | const catchAndLog = require('../../lib/helpers/catchAndLog') 7 | const isIgnoringDotenvKeys = require('../../lib/helpers/isIgnoringDotenvKeys') 8 | 9 | function encrypt () { 10 | const options = this.opts() 11 | logger.debug(`options: ${JSON.stringify(options)}`) 12 | 13 | const envs = this.envs 14 | 15 | // stdout - should not have a try so that exit codes can surface to stdout 16 | if (options.stdout) { 17 | const { 18 | processedEnvs 19 | } = new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile).run() 20 | 21 | for (const processedEnv of processedEnvs) { 22 | console.log(processedEnv.envSrc) 23 | } 24 | process.exit(0) // exit early 25 | } else { 26 | try { 27 | const { 28 | processedEnvs, 29 | changedFilepaths, 30 | unchangedFilepaths 31 | } = new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile).run() 32 | 33 | for (const processedEnv of processedEnvs) { 34 | logger.verbose(`encrypting ${processedEnv.envFilepath} (${processedEnv.filepath})`) 35 | if (processedEnv.error) { 36 | if (processedEnv.error.code === 'MISSING_ENV_FILE') { 37 | logger.warn(processedEnv.error.message) 38 | logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx encrypt]`) 39 | } else { 40 | logger.warn(processedEnv.error.message) 41 | if (processedEnv.error.help) { 42 | logger.help(processedEnv.error.help) 43 | } 44 | } 45 | } else if (processedEnv.changed) { 46 | fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc) 47 | 48 | logger.verbose(`encrypted ${processedEnv.envFilepath} (${processedEnv.filepath})`) 49 | } else { 50 | logger.verbose(`no changes ${processedEnv.envFilepath} (${processedEnv.filepath})`) 51 | } 52 | } 53 | 54 | if (changedFilepaths.length > 0) { 55 | logger.success(`✔ encrypted (${changedFilepaths.join(',')})`) 56 | } else if (unchangedFilepaths.length > 0) { 57 | logger.info(`no changes (${unchangedFilepaths})`) 58 | } else { 59 | // do nothing - scenario when no .env files found 60 | } 61 | 62 | for (const processedEnv of processedEnvs) { 63 | if (processedEnv.privateKeyAdded) { 64 | logger.success(`✔ key added to .env.keys (${processedEnv.privateKeyName})`) 65 | 66 | if (!isIgnoringDotenvKeys()) { 67 | logger.help('⮕ next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys') 68 | } 69 | 70 | logger.help(`⮕ next run [${processedEnv.privateKeyName}='${processedEnv.privateKey}' dotenvx run -- yourcommand] to test decryption locally`) 71 | } 72 | } 73 | } catch (error) { 74 | catchAndLog(error) 75 | process.exit(1) 76 | } 77 | } 78 | } 79 | 80 | module.exports = encrypt 81 | -------------------------------------------------------------------------------- /src/cli/actions/ext/genexample.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./../../../lib/helpers/fsx') 2 | const main = require('./../../../lib/main') 3 | const { logger } = require('./../../../shared/logger') 4 | 5 | function genexample (directory) { 6 | logger.debug(`directory: ${directory}`) 7 | 8 | const options = this.opts() 9 | logger.debug(`options: ${JSON.stringify(options)}`) 10 | 11 | try { 12 | const { 13 | envExampleFile, 14 | envFile, 15 | exampleFilepath, 16 | addedKeys 17 | } = main.genexample(directory, options.envFile) 18 | 19 | logger.verbose(`loading env from ${envFile}`) 20 | 21 | fsx.writeFileX(exampleFilepath, envExampleFile) 22 | 23 | if (addedKeys.length > 0) { 24 | logger.success(`updated .env.example (${addedKeys.length})`) 25 | } else { 26 | logger.info('no changes (.env.example)') 27 | } 28 | } catch (error) { 29 | logger.error(error.message) 30 | if (error.help) { 31 | logger.help(error.help) 32 | } 33 | if (error.code) { 34 | logger.debug(`ERROR_CODE: ${error.code}`) 35 | } 36 | process.exit(1) 37 | } 38 | } 39 | 40 | module.exports = genexample 41 | -------------------------------------------------------------------------------- /src/cli/actions/ext/gitignore.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./../../../lib/helpers/fsx') 2 | 3 | const DEFAULT_PATTERNS = ['.env*'] 4 | const { logger } = require('./../../../shared/logger') 5 | 6 | class Generic { 7 | constructor (filename, patterns = DEFAULT_PATTERNS, touchFile = false) { 8 | this.filename = filename 9 | this.patterns = patterns 10 | this.touchFile = touchFile 11 | } 12 | 13 | append (str) { 14 | fsx.appendFileSync(this.filename, `\n${str}`) 15 | } 16 | 17 | run () { 18 | const changedPatterns = [] 19 | if (!fsx.existsSync(this.filename)) { 20 | if (this.touchFile === true && this.patterns.length > 0) { 21 | fsx.writeFileX(this.filename, '') 22 | } else { 23 | return 24 | } 25 | } 26 | 27 | const lines = fsx.readFileX(this.filename).split(/\r?\n/) 28 | this.patterns.forEach(pattern => { 29 | if (!lines.includes(pattern.trim())) { 30 | this.append(pattern) 31 | 32 | changedPatterns.push(pattern.trim()) 33 | } 34 | }) 35 | 36 | if (changedPatterns.length > 0) { 37 | logger.success(`✔ ignored ${this.patterns} (${this.filename})`) 38 | } else { 39 | logger.info(`no changes (${this.filename})`) 40 | } 41 | } 42 | } 43 | 44 | class Git { 45 | constructor (patterns = DEFAULT_PATTERNS) { 46 | this.patterns = patterns 47 | } 48 | 49 | run () { 50 | logger.verbose('add to .gitignore') 51 | new Generic('.gitignore', this.patterns, true).run() 52 | } 53 | } 54 | 55 | class Docker { 56 | constructor (patterns = DEFAULT_PATTERNS) { 57 | this.patterns = patterns 58 | } 59 | 60 | run () { 61 | logger.verbose('add to .dockerignore (if exists)') 62 | new Generic('.dockerignore', this.patterns).run() 63 | } 64 | } 65 | 66 | class Npm { 67 | constructor (patterns = DEFAULT_PATTERNS) { 68 | this.patterns = patterns 69 | } 70 | 71 | run () { 72 | logger.verbose('add to .npmignore (if existing)') 73 | new Generic('.npmignore', this.patterns).run() 74 | } 75 | } 76 | 77 | class Vercel { 78 | constructor (patterns = DEFAULT_PATTERNS) { 79 | this.patterns = patterns 80 | } 81 | 82 | run () { 83 | logger.verbose('add to .vercelignore (if existing)') 84 | new Generic('.vercelignore', this.patterns).run() 85 | } 86 | } 87 | 88 | function gitignore () { 89 | const options = this.opts() 90 | logger.debug(`options: ${JSON.stringify(options)}`) 91 | 92 | const patterns = options.pattern 93 | 94 | new Git(patterns).run() 95 | new Docker(patterns).run() 96 | new Npm(patterns).run() 97 | new Vercel(patterns).run() 98 | } 99 | 100 | module.exports = gitignore 101 | module.exports.Git = Git 102 | module.exports.Docker = Docker 103 | module.exports.Npm = Npm 104 | module.exports.Vercel = Vercel 105 | module.exports.Generic = Generic 106 | -------------------------------------------------------------------------------- /src/cli/actions/ext/prebuild.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('./../../../shared/logger') 2 | 3 | const Prebuild = require('./../../../lib/services/prebuild') 4 | 5 | function prebuild (directory) { 6 | // debug args 7 | logger.debug(`directory: ${directory}`) 8 | 9 | const options = this.opts() 10 | logger.debug(`options: ${JSON.stringify(options)}`) 11 | 12 | try { 13 | const { 14 | successMessage, 15 | warnings 16 | } = new Prebuild(directory, options).run() 17 | 18 | for (const warning of warnings) { 19 | logger.warn(warning.message) 20 | if (warning.help) { 21 | logger.help(warning.help) 22 | } 23 | } 24 | 25 | logger.success(successMessage) 26 | } catch (error) { 27 | logger.error(error.message) 28 | if (error.help) { 29 | logger.help(error.help) 30 | } 31 | 32 | process.exit(1) 33 | } 34 | } 35 | 36 | module.exports = prebuild 37 | -------------------------------------------------------------------------------- /src/cli/actions/ext/precommit.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('./../../../shared/logger') 2 | 3 | const Precommit = require('./../../../lib/services/precommit') 4 | 5 | function precommit (directory) { 6 | // debug args 7 | logger.debug(`directory: ${directory}`) 8 | 9 | const options = this.opts() 10 | logger.debug(`options: ${JSON.stringify(options)}`) 11 | 12 | try { 13 | const { 14 | successMessage, 15 | warnings 16 | } = new Precommit(directory, options).run() 17 | 18 | for (const warning of warnings) { 19 | logger.warn(warning.message) 20 | if (warning.help) { 21 | logger.help(warning.help) 22 | } 23 | } 24 | 25 | logger.success(successMessage) 26 | } catch (error) { 27 | logger.error(error.message) 28 | if (error.help) { 29 | logger.help(error.help) 30 | } 31 | 32 | process.exit(1) 33 | } 34 | } 35 | 36 | module.exports = precommit 37 | -------------------------------------------------------------------------------- /src/cli/actions/ext/scan.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process') 2 | 3 | const { logger } = require('./../../../shared/logger') 4 | const chomp = require('./../../../lib/helpers/chomp') 5 | 6 | function scan () { 7 | const options = this.opts() 8 | logger.debug(`options: ${JSON.stringify(options)}`) 9 | 10 | try { 11 | // redirect stderr to stdout to capture and ignore it 12 | childProcess.execSync('gitleaks version', { stdio: ['ignore', 'pipe', 'ignore'] }) 13 | } catch (error) { 14 | logger.error('gitleaks: command not found') 15 | logger.help('? install gitleaks: [brew install gitleaks]') 16 | logger.help('? other install options: [https://github.com/gitleaks/gitleaks]') 17 | process.exit(1) 18 | return 19 | } 20 | 21 | let output = '' 22 | try { 23 | output = childProcess.execSync('gitleaks detect --no-banner --verbose 2>&1').toString() // gitleaks sends exit code 1 but puts data on stdout for failures, so we catch later and resurface the stdout 24 | logger.info(chomp(output)) 25 | } catch (error) { 26 | if (error.stdout) { 27 | logger.error(chomp(error.stdout.toString())) 28 | } 29 | 30 | process.exit(1) 31 | } 32 | } 33 | 34 | module.exports = scan 35 | -------------------------------------------------------------------------------- /src/cli/actions/get.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('./../../shared/logger') 2 | 3 | const conventions = require('./../../lib/helpers/conventions') 4 | const escape = require('./../../lib/helpers/escape') 5 | 6 | const Get = require('./../../lib/services/get') 7 | 8 | function get (key) { 9 | if (key) { 10 | logger.debug(`key: ${key}`) 11 | } 12 | 13 | const options = this.opts() 14 | logger.debug(`options: ${JSON.stringify(options)}`) 15 | 16 | const ignore = options.ignore || [] 17 | 18 | let envs = [] 19 | // handle shorthand conventions - like --convention=nextjs 20 | if (options.convention) { 21 | envs = conventions(options.convention).concat(this.envs) 22 | } else { 23 | envs = this.envs 24 | } 25 | 26 | try { 27 | const { parsed, errors } = new Get(key, envs, options.overload, process.env.DOTENV_KEY, options.all, options.envKeysFile).run() 28 | 29 | for (const error of errors || []) { 30 | if (options.strict) throw error // throw immediately if strict 31 | 32 | if (ignore.includes(error.code)) { 33 | continue // ignore error 34 | } 35 | 36 | logger.error(error.message) 37 | if (error.help) { 38 | logger.error(error.help) 39 | } 40 | } 41 | 42 | if (key) { 43 | const single = parsed[key] 44 | if (single === undefined) { 45 | console.log('') 46 | } else { 47 | console.log(single) 48 | } 49 | } else { 50 | if (options.format === 'eval') { 51 | let inline = '' 52 | for (const [key, value] of Object.entries(parsed)) { 53 | inline += `${key}=${escape(value)}\n` 54 | } 55 | inline = inline.trim() 56 | 57 | console.log(inline) 58 | } else if (options.format === 'shell') { 59 | let inline = '' 60 | for (const [key, value] of Object.entries(parsed)) { 61 | inline += `${key}=${value} ` 62 | } 63 | inline = inline.trim() 64 | 65 | console.log(inline) 66 | } else { 67 | let space = 0 68 | if (options.prettyPrint) { 69 | space = 2 70 | } 71 | 72 | console.log(JSON.stringify(parsed, null, space)) 73 | } 74 | } 75 | } catch (error) { 76 | logger.error(error.message) 77 | if (error.help) { 78 | logger.error(error.help) 79 | } 80 | process.exit(1) 81 | } 82 | } 83 | 84 | module.exports = get 85 | -------------------------------------------------------------------------------- /src/cli/actions/keypair.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('./../../shared/logger') 2 | 3 | const main = require('./../../lib/main') 4 | 5 | function keypair (key) { 6 | if (key) { 7 | logger.debug(`key: ${key}`) 8 | } 9 | 10 | const options = this.opts() 11 | logger.debug(`options: ${JSON.stringify(options)}`) 12 | 13 | const results = main.keypair(options.envFile, key, options.envKeysFile) 14 | 15 | if (typeof results === 'object' && results !== null) { 16 | // inline shell format - env $(dotenvx keypair --format=shell) your-command 17 | if (options.format === 'shell') { 18 | let inline = '' 19 | for (const [key, value] of Object.entries(results)) { 20 | inline += `${key}=${value || ''} ` 21 | } 22 | inline = inline.trim() 23 | 24 | console.log(inline) 25 | // json format 26 | } else { 27 | let space = 0 28 | if (options.prettyPrint) { 29 | space = 2 30 | } 31 | 32 | console.log(JSON.stringify(results, null, space)) 33 | } 34 | } else { 35 | if (results === undefined) { 36 | console.log('') 37 | process.exit(1) 38 | } else { 39 | console.log(results) 40 | } 41 | } 42 | } 43 | 44 | module.exports = keypair 45 | -------------------------------------------------------------------------------- /src/cli/actions/ls.js: -------------------------------------------------------------------------------- 1 | const treeify = require('object-treeify') 2 | 3 | const { logger } = require('./../../shared/logger') 4 | 5 | const main = require('./../../lib/main') 6 | const ArrayToTree = require('./../../lib/helpers/arrayToTree') 7 | 8 | function ls (directory) { 9 | // debug args 10 | logger.debug(`directory: ${directory}`) 11 | 12 | const options = this.opts() 13 | logger.debug(`options: ${JSON.stringify(options)}`) 14 | 15 | const filepaths = main.ls(directory, options.envFile, options.excludeEnvFile) 16 | logger.debug(`filepaths: ${JSON.stringify(filepaths)}`) 17 | 18 | const tree = new ArrayToTree(filepaths).run() 19 | logger.debug(`tree: ${JSON.stringify(tree)}`) 20 | 21 | logger.info(treeify(tree)) 22 | } 23 | 24 | module.exports = ls 25 | -------------------------------------------------------------------------------- /src/cli/actions/rotate.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./../../lib/helpers/fsx') 2 | const { logger } = require('./../../shared/logger') 3 | 4 | const Rotate = require('./../../lib/services/rotate') 5 | 6 | const catchAndLog = require('../../lib/helpers/catchAndLog') 7 | const isIgnoringDotenvKeys = require('../../lib/helpers/isIgnoringDotenvKeys') 8 | 9 | function rotate () { 10 | const options = this.opts() 11 | logger.debug(`options: ${JSON.stringify(options)}`) 12 | 13 | const envs = this.envs 14 | 15 | // stdout - should not have a try so that exit codes can surface to stdout 16 | if (options.stdout) { 17 | const { 18 | processedEnvs 19 | } = new Rotate(envs, options.key, options.excludeKey, options.envKeysFile).run() 20 | 21 | for (const processedEnv of processedEnvs) { 22 | console.log(processedEnv.envSrc) 23 | console.log('') 24 | console.log(processedEnv.envKeysSrc) 25 | } 26 | process.exit(0) // exit early 27 | } else { 28 | try { 29 | const { 30 | processedEnvs, 31 | changedFilepaths, 32 | unchangedFilepaths 33 | } = new Rotate(envs, options.key, options.excludeKey, options.envKeysFile).run() 34 | 35 | for (const processedEnv of processedEnvs) { 36 | logger.verbose(`rotating ${processedEnv.envFilepath} (${processedEnv.filepath})`) 37 | if (processedEnv.error) { 38 | if (processedEnv.error.code === 'MISSING_ENV_FILE') { 39 | logger.warn(processedEnv.error.message) 40 | logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx rotate]`) 41 | } else { 42 | logger.warn(processedEnv.error.message) 43 | if (processedEnv.error.help) { 44 | logger.help(processedEnv.error.help) 45 | } 46 | } 47 | } else if (processedEnv.changed) { 48 | fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc) 49 | fsx.writeFileX(processedEnv.envKeysFilepath, processedEnv.envKeysSrc) 50 | 51 | logger.verbose(`rotated ${processedEnv.envFilepath} (${processedEnv.filepath})`) 52 | } else { 53 | logger.verbose(`no changes ${processedEnv.envFilepath} (${processedEnv.filepath})`) 54 | } 55 | } 56 | 57 | if (changedFilepaths.length > 0) { 58 | logger.success(`✔ rotated (${changedFilepaths.join(',')})`) 59 | } else if (unchangedFilepaths.length > 0) { 60 | logger.info(`no changes (${unchangedFilepaths})`) 61 | } else { 62 | // do nothing - scenario when no .env files found 63 | } 64 | 65 | for (const processedEnv of processedEnvs) { 66 | if (processedEnv.privateKeyAdded) { 67 | logger.success(`✔ key added to .env.keys (${processedEnv.privateKeyName})`) 68 | 69 | if (!isIgnoringDotenvKeys()) { 70 | logger.help('⮕ next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys') 71 | } 72 | 73 | logger.help(`⮕ next run [${processedEnv.privateKeyName}='${processedEnv.privateKey}' dotenvx get] to test decryption locally`) 74 | } 75 | } 76 | } catch (error) { 77 | catchAndLog(error) 78 | process.exit(1) 79 | } 80 | } 81 | } 82 | 83 | module.exports = rotate 84 | -------------------------------------------------------------------------------- /src/cli/actions/set.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./../../lib/helpers/fsx') 2 | const { logger } = require('./../../shared/logger') 3 | 4 | const Sets = require('./../../lib/services/sets') 5 | 6 | const catchAndLog = require('../../lib/helpers/catchAndLog') 7 | const isIgnoringDotenvKeys = require('../../lib/helpers/isIgnoringDotenvKeys') 8 | 9 | function set (key, value) { 10 | logger.debug(`key: ${key}`) 11 | logger.debug(`value: ${value}`) 12 | 13 | const options = this.opts() 14 | logger.debug(`options: ${JSON.stringify(options)}`) 15 | 16 | // encrypt 17 | let encrypt = true 18 | if (options.plain) { 19 | encrypt = false 20 | } 21 | 22 | try { 23 | const envs = this.envs 24 | const envKeysFilepath = options.envKeysFile 25 | 26 | const { 27 | processedEnvs, 28 | changedFilepaths, 29 | unchangedFilepaths 30 | } = new Sets(key, value, envs, encrypt, envKeysFilepath).run() 31 | 32 | let withEncryption = '' 33 | 34 | if (encrypt) { 35 | withEncryption = ' with encryption' 36 | } 37 | 38 | for (const processedEnv of processedEnvs) { 39 | logger.verbose(`setting for ${processedEnv.envFilepath}`) 40 | 41 | if (processedEnv.error) { 42 | if (processedEnv.error.code === 'MISSING_ENV_FILE') { 43 | logger.warn(processedEnv.error.message) 44 | logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx set]`) 45 | } else { 46 | logger.warn(processedEnv.error.message) 47 | if (processedEnv.error.help) { 48 | logger.help(processedEnv.error.help) 49 | } 50 | } 51 | } else { 52 | fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc) 53 | 54 | logger.verbose(`${processedEnv.key} set${withEncryption} (${processedEnv.envFilepath})`) 55 | logger.debug(`${processedEnv.key} set${withEncryption} to ${processedEnv.value} (${processedEnv.envFilepath})`) 56 | } 57 | } 58 | 59 | if (changedFilepaths.length > 0) { 60 | logger.success(`✔ set ${key}${withEncryption} (${changedFilepaths.join(',')})`) 61 | } else if (unchangedFilepaths.length > 0) { 62 | logger.info(`no changes (${unchangedFilepaths})`) 63 | } else { 64 | // do nothing 65 | } 66 | 67 | for (const processedEnv of processedEnvs) { 68 | if (processedEnv.privateKeyAdded) { 69 | logger.success(`✔ key added to ${processedEnv.envKeysFilepath} (${processedEnv.privateKeyName})`) 70 | 71 | if (!isIgnoringDotenvKeys()) { 72 | logger.help('⮕ next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys') 73 | } 74 | 75 | logger.help(`⮕ next run [${processedEnv.privateKeyName}='${processedEnv.privateKey}' dotenvx get ${key}] to test decryption locally`) 76 | } 77 | } 78 | } catch (error) { 79 | catchAndLog(error) 80 | process.exit(1) 81 | } 82 | } 83 | 84 | module.exports = set 85 | -------------------------------------------------------------------------------- /src/cli/commands/ext.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('commander') 2 | 3 | const examples = require('./../examples') 4 | const executeExtension = require('../../lib/helpers/executeExtension') 5 | const removeDynamicHelpSection = require('../../lib/helpers/removeDynamicHelpSection') 6 | 7 | const ext = new Command('ext') 8 | 9 | ext 10 | .description('🔌 extensions') 11 | .allowUnknownOption() 12 | 13 | // list known extensions here you want to display 14 | ext.addHelpText('after', ' vault 🔐 manage .env.vault files') 15 | 16 | ext 17 | .argument('[command]', 'dynamic ext command') 18 | .argument('[args...]', 'dynamic ext command arguments') 19 | .action((command, args, cmdObj) => { 20 | const rawArgs = process.argv.slice(3) // adjust the index based on where actual args start 21 | executeExtension(ext, command, rawArgs) 22 | }) 23 | 24 | // dotenvx ext ls 25 | ext.command('ls') 26 | .description('print all .env files in a tree structure') 27 | .argument('[directory]', 'directory to list .env files from', '.') 28 | .option('-f, --env-file <filenames...>', 'path(s) to your env file(s)', '.env*') 29 | .option('-ef, --exclude-env-file <excludeFilenames...>', 'path(s) to exclude from your env file(s) (default: none)') 30 | .action(require('./../actions/ls')) 31 | 32 | // dotenvx ext genexample 33 | ext.command('genexample') 34 | .description('generate .env.example') 35 | .argument('[directory]', 'directory to generate from', '.') 36 | .option('-f, --env-file <paths...>', 'path(s) to your env file(s)', '.env') 37 | .action(require('./../actions/ext/genexample')) 38 | 39 | // dotenvx ext gitignore 40 | ext.command('gitignore') 41 | .description('append to .gitignore file (and if existing, .dockerignore, .npmignore, and .vercelignore)') 42 | .addHelpText('after', examples.gitignore) 43 | .option('--pattern <patterns...>', 'pattern(s) to gitignore', ['.env*']) 44 | .action(require('./../actions/ext/gitignore')) 45 | 46 | // dotenvx ext prebuild 47 | ext.command('prebuild') 48 | .description('prevent including .env files in docker builds') 49 | .addHelpText('after', examples.prebuild) 50 | .argument('[directory]', 'directory to prevent including .env files from', '.') 51 | .action(require('./../actions/ext/prebuild')) 52 | 53 | // dotenvx ext precommit 54 | ext.command('precommit') 55 | .description('prevent committing .env files to code') 56 | .addHelpText('after', examples.precommit) 57 | .argument('[directory]', 'directory to prevent committing .env files from', '.') 58 | .option('-i, --install', 'install to .git/hooks/pre-commit') 59 | .action(require('./../actions/ext/precommit')) 60 | 61 | // dotenvx scan 62 | ext.command('scan') 63 | .description('scan for leaked secrets') 64 | .action(require('./../actions/ext/scan')) 65 | 66 | // override helpInformation to hide dynamic commands 67 | ext.helpInformation = function () { 68 | const originalHelp = Command.prototype.helpInformation.call(this) 69 | const lines = originalHelp.split('\n') 70 | 71 | removeDynamicHelpSection(lines) 72 | 73 | return lines.join('\n') 74 | } 75 | 76 | module.exports = ext 77 | -------------------------------------------------------------------------------- /src/cli/examples.js: -------------------------------------------------------------------------------- 1 | const run = function () { 2 | return ` 3 | Examples: 4 | 5 | \`\`\` 6 | $ dotenvx run -- npm run dev 7 | $ dotenvx run -- flask --app index run 8 | $ dotenvx run -- php artisan serve 9 | $ dotenvx run -- bin/rails s 10 | \`\`\` 11 | 12 | Try it: 13 | 14 | \`\`\` 15 | $ echo "HELLO=World" > .env 16 | $ echo "console.log('Hello ' + process.env.HELLO)" > index.js 17 | 18 | $ dotenvx run -f .env -- node index.js 19 | [dotenvx] injecting env (1) from .env 20 | Hello World 21 | \`\`\` 22 | ` 23 | } 24 | 25 | const precommit = function () { 26 | return ` 27 | Examples: 28 | 29 | \`\`\` 30 | $ dotenvx ext precommit 31 | $ dotenvx ext precommit --install 32 | \`\`\` 33 | 34 | Try it: 35 | 36 | \`\`\` 37 | $ dotenvx ext precommit 38 | [dotenvx@0.45.0][precommit] success 39 | \`\`\` 40 | ` 41 | } 42 | 43 | const prebuild = function () { 44 | return ` 45 | Examples: 46 | 47 | \`\`\` 48 | $ dotenvx ext prebuild 49 | \`\`\` 50 | 51 | Try it: 52 | 53 | \`\`\` 54 | $ dotenvx ext prebuild 55 | [dotenvx@0.10.0][prebuild] success 56 | \`\`\` 57 | ` 58 | } 59 | 60 | const gitignore = function () { 61 | return ` 62 | Examples: 63 | 64 | \`\`\` 65 | $ dotenvx ext gitignore 66 | $ dotenvx ext gitignore --pattern .env.keys 67 | \`\`\` 68 | 69 | Try it: 70 | 71 | \`\`\` 72 | $ dotenvx ext gitignore 73 | ✔ ignored .env* (.gitignore) 74 | \`\`\` 75 | ` 76 | } 77 | 78 | const set = function () { 79 | return ` 80 | Examples: 81 | 82 | \`\`\` 83 | $ dotenvx set KEY value 84 | $ dotenvx set KEY "value with spaces" 85 | $ dotenvx set KEY -- "---value with a dash---" 86 | $ dotenvx set KEY -- "-----BEGIN OPENSSH PRIVATE KEY----- 87 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 88 | -----END OPENSSH PRIVATE KEY-----" 89 | \`\`\` 90 | ` 91 | } 92 | 93 | module.exports = { 94 | run, 95 | precommit, 96 | prebuild, 97 | gitignore, 98 | set 99 | } 100 | -------------------------------------------------------------------------------- /src/lib/config.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /src/lib/config.js: -------------------------------------------------------------------------------- 1 | require('./main.js').config() 2 | -------------------------------------------------------------------------------- /src/lib/helpers/append.js: -------------------------------------------------------------------------------- 1 | const quotes = require('./quotes') 2 | const dotenvParse = require('./dotenvParse') 3 | const escapeForRegex = require('./escapeForRegex') 4 | const escapeDollarSigns = require('./escapeDollarSigns') 5 | 6 | function append (src, key, appendValue) { 7 | let output 8 | let newPart = '' 9 | 10 | const parsed = dotenvParse(src, true, true) // skip expanding \n and skip converting \r\n 11 | const _quotes = quotes(src) 12 | if (Object.prototype.hasOwnProperty.call(parsed, key)) { 13 | const quote = _quotes[key] 14 | const originalValue = parsed[key] 15 | 16 | newPart += `${key}=${quote}${originalValue},${appendValue}${quote}` 17 | 18 | const escapedOriginalValue = escapeForRegex(originalValue) 19 | 20 | // conditionally enforce end of line 21 | let enforceEndOfLine = '' 22 | if (escapedOriginalValue === '') { 23 | enforceEndOfLine = '#39; // EMPTY scenario 24 | } 25 | 26 | const currentPart = new RegExp( 27 | '^' + // start of line 28 | '(\\s*)?' + // spaces 29 | '(export\\s+)?' + // export 30 | key + // KEY 31 | '\\s*=\\s*' + // spaces (KEY = value) 32 | '["\'`]?' + // open quote 33 | escapedOriginalValue + // escaped value 34 | '["\'`]?' + // close quote 35 | enforceEndOfLine 36 | , 37 | 'gm' // (g)lobal (m)ultiline 38 | ) 39 | 40 | const saferInput = escapeDollarSigns(newPart) // cleanse user inputted capture groups ($1, $2 etc) 41 | 42 | // $1 preserves spaces 43 | // $2 preserves export 44 | output = src.replace(currentPart, `$1$2${saferInput}`) 45 | } else { 46 | newPart += `${key}="${appendValue}"` 47 | 48 | // append 49 | if (src.endsWith('\n')) { 50 | newPart = newPart + '\n' 51 | } else { 52 | newPart = '\n' + newPart 53 | } 54 | 55 | output = src + newPart 56 | } 57 | 58 | return output 59 | } 60 | 61 | module.exports = append 62 | -------------------------------------------------------------------------------- /src/lib/helpers/arrayToTree.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | class ArrayToTree { 4 | constructor (arr) { 5 | this.arr = arr 6 | } 7 | 8 | run () { 9 | const tree = {} 10 | 11 | for (let i = 0; i < this.arr.length; i++) { 12 | const normalizedPath = path.normalize(this.arr[i]) // normalize any strange paths 13 | const parts = normalizedPath.split(path.sep) // use the platform-specific path segment separator 14 | let current = tree 15 | 16 | for (let j = 0; j < parts.length; j++) { 17 | const part = parts[j] 18 | current[part] = current[part] || {} 19 | current = current[part] 20 | } 21 | } 22 | 23 | return tree 24 | } 25 | } 26 | 27 | module.exports = ArrayToTree 28 | -------------------------------------------------------------------------------- /src/lib/helpers/buildEnvs.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const conventions = require('./conventions') 4 | const dotenvOptionPaths = require('./dotenvOptionPaths') 5 | const DeprecationNotice = require('./deprecationNotice') 6 | 7 | function buildEnvs (options, DOTENV_KEY = undefined) { 8 | // build envs using user set option.path 9 | const optionPaths = dotenvOptionPaths(options) // [ '.env' ] 10 | 11 | let envs = [] 12 | if (options.convention) { // handle shorthand conventions 13 | envs = conventions(options.convention).concat(envs) 14 | } 15 | 16 | new DeprecationNotice({ DOTENV_KEY }).dotenvKey() // DEPRECATION NOTICE 17 | 18 | for (const optionPath of optionPaths) { 19 | // if DOTENV_KEY is set then assume we are checking envVaultFile 20 | if (DOTENV_KEY) { 21 | envs.push({ 22 | type: 'envVaultFile', 23 | value: path.join(path.dirname(optionPath), '.env.vault') 24 | }) 25 | } else { 26 | envs.push({ type: 'envFile', value: optionPath }) 27 | } 28 | } 29 | 30 | return envs 31 | } 32 | 33 | module.exports = buildEnvs 34 | -------------------------------------------------------------------------------- /src/lib/helpers/catchAndLog.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('./../../shared/logger') 2 | 3 | function catchAndLog (error) { 4 | logger.error(error.message) 5 | if (error.help) { 6 | logger.help(error.help) 7 | } 8 | if (error.debug) { 9 | logger.debug(error.debug) 10 | } 11 | if (error.code) { 12 | logger.debug(`ERROR_CODE: ${error.code}`) 13 | } 14 | } 15 | 16 | module.exports = catchAndLog 17 | -------------------------------------------------------------------------------- /src/lib/helpers/chomp.js: -------------------------------------------------------------------------------- 1 | function chomp (value) { 2 | return value.replace(/[\r\n]+$/, '') 3 | } 4 | 5 | module.exports = chomp 6 | -------------------------------------------------------------------------------- /src/lib/helpers/colorDepth.js: -------------------------------------------------------------------------------- 1 | const { WriteStream } = require('tty') 2 | 3 | const getColorDepth = () => { 4 | try { 5 | return WriteStream.prototype.getColorDepth() 6 | } catch (error) { 7 | const term = process.env.TERM 8 | 9 | if (term && (term.includes('256color') || term.includes('xterm'))) { 10 | return 8 // 256 colors 11 | } 12 | 13 | return 4 14 | } 15 | } 16 | 17 | module.exports = { getColorDepth } 18 | -------------------------------------------------------------------------------- /src/lib/helpers/conventions.js: -------------------------------------------------------------------------------- 1 | function conventions (convention) { 2 | const env = process.env.DOTENV_ENV || process.env.NODE_ENV || 'development' 3 | 4 | if (convention === 'nextjs') { 5 | const canonicalEnv = ['development', 'test', 'production'].includes(env) && env 6 | 7 | return [ 8 | canonicalEnv && { type: 'envFile', value: `.env.${canonicalEnv}.local` }, 9 | canonicalEnv !== 'test' && { type: 'envFile', value: '.env.local' }, 10 | canonicalEnv && { type: 'envFile', value: `.env.${canonicalEnv}` }, 11 | { type: 'envFile', value: '.env' } 12 | ].filter(Boolean) 13 | } else if (convention === 'flow') { 14 | return [ 15 | { type: 'envFile', value: `.env.${env}.local` }, 16 | { type: 'envFile', value: `.env.${env}` }, 17 | { type: 'envFile', value: '.env.local' }, 18 | { type: 'envFile', value: '.env' }, 19 | { type: 'envFile', value: '.env.defaults' } 20 | ] 21 | } else { 22 | throw new Error(`INVALID_CONVENTION: '${convention}'. permitted conventions: ['nextjs', 'flow']`) 23 | } 24 | } 25 | 26 | module.exports = conventions 27 | -------------------------------------------------------------------------------- /src/lib/helpers/decrypt.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | 3 | const parseEncryptionKeyFromDotenvKey = require('./parseEncryptionKeyFromDotenvKey') 4 | 5 | function decrypt (ciphertext, dotenvKey) { 6 | const key = parseEncryptionKeyFromDotenvKey(dotenvKey) 7 | 8 | try { 9 | return dotenv.decrypt(ciphertext, key) 10 | } catch (e) { 11 | if (e.code === 'DECRYPTION_FAILED') { 12 | const error = new Error('[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.') 13 | error.code = 'DECRYPTION_FAILED' 14 | error.help = '[DECRYPTION_FAILED] Run with debug flag [dotenvx run --debug -- yourcommand] or manually run [echo $DOTENV_KEY] to compare it to the one in .env.keys.' 15 | error.debug = `[DECRYPTION_FAILED] DOTENV_KEY is ${dotenvKey}` 16 | throw error 17 | } 18 | 19 | if (e.code === 'ERR_CRYPTO_INVALID_AUTH_TAG') { 20 | const error = new Error('[INVALID_CIPHERTEXT] Unable to decrypt what appears to be invalid ciphertext.') 21 | error.code = 'INVALID_CIPHERTEXT' 22 | error.help = '[INVALID_CIPHERTEXT] Run with debug flag [dotenvx run --debug -- yourcommand] or manually check .env.vault.' 23 | error.debug = `[INVALID_CIPHERTEXT] ciphertext is '${ciphertext}'` 24 | throw error 25 | } 26 | 27 | throw e 28 | } 29 | } 30 | 31 | module.exports = decrypt 32 | -------------------------------------------------------------------------------- /src/lib/helpers/decryptKeyValue.js: -------------------------------------------------------------------------------- 1 | const { decrypt } = require('eciesjs') 2 | 3 | const Errors = require('./errors') 4 | 5 | const PREFIX = 'encrypted:' 6 | 7 | function decryptKeyValue (key, value, privateKeyName, privateKey) { 8 | let decryptedValue 9 | let decryptionError 10 | 11 | if (!value.startsWith(PREFIX)) { 12 | return value 13 | } 14 | 15 | privateKey = privateKey || '' 16 | if (privateKey.length <= 0) { 17 | decryptionError = new Errors({ key, privateKeyName, privateKey }).missingPrivateKey() 18 | } else { 19 | const privateKeys = privateKey.split(',') 20 | for (const privKey of privateKeys) { 21 | const secret = Buffer.from(privKey, 'hex') 22 | const encoded = value.substring(PREFIX.length) 23 | const ciphertext = Buffer.from(encoded, 'base64') 24 | 25 | try { 26 | decryptedValue = decrypt(secret, ciphertext).toString() 27 | decryptionError = null // reset to null error (scenario for multiple private keys) 28 | break 29 | } catch (e) { 30 | if (e.message === 'Invalid private key') { 31 | decryptionError = new Errors({ key, privateKeyName, privateKey }).invalidPrivateKey() 32 | } else if (e.message === 'Unsupported state or unable to authenticate data') { 33 | decryptionError = new Errors({ key, privateKeyName, privateKey }).looksWrongPrivateKey() 34 | } else if (e.message === 'Point of length 65 was invalid. Expected 33 compressed bytes or 65 uncompressed bytes') { 35 | decryptionError = new Errors({ key, privateKeyName, privateKey }).malformedEncryptedData() 36 | } else { 37 | decryptionError = new Errors({ key, privateKeyName, privateKey, message: e.message }).decryptionFailed() 38 | } 39 | } 40 | } 41 | } 42 | 43 | if (decryptionError) { 44 | throw decryptionError 45 | } 46 | 47 | return decryptedValue 48 | } 49 | 50 | module.exports = decryptKeyValue 51 | -------------------------------------------------------------------------------- /src/lib/helpers/deprecationNotice.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('./../../shared/logger') 2 | 3 | class DeprecationNotice { 4 | constructor (options = {}) { 5 | this.DOTENV_KEY = options.DOTENV_KEY || process.env.DOTENV_KEY 6 | } 7 | 8 | dotenvKey () { 9 | if (this.DOTENV_KEY) { 10 | logger.warn('[DEPRECATION NOTICE] Setting DOTENV_KEY with .env.vault is deprecated.') 11 | logger.warn('[DEPRECATION NOTICE] Run [dotenvx ext vault migrate] for instructions on converting your .env.vault file to encrypted .env files (using public key encryption algorithm secp256k1)') 12 | logger.warn('[DEPRECATION NOTICE] Read more at [https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md#0380]') 13 | } 14 | } 15 | } 16 | 17 | module.exports = DeprecationNotice 18 | -------------------------------------------------------------------------------- /src/lib/helpers/detectEncoding.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | function detectEncoding (filepath) { 4 | const buffer = fs.readFileSync(filepath) 5 | 6 | // check for UTF-16LE BOM (Byte Order Mark) 7 | if (buffer.length >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) { 8 | return 'utf16le' 9 | } 10 | 11 | /* c8 ignore start */ 12 | // check for UTF-8 BOM 13 | if (buffer.length >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { 14 | return 'utf8' 15 | } 16 | 17 | /* c8 ignore stop */ 18 | 19 | return 'utf8' 20 | } 21 | 22 | module.exports = detectEncoding 23 | -------------------------------------------------------------------------------- /src/lib/helpers/determineEnvs.js: -------------------------------------------------------------------------------- 1 | const dotenvPrivateKeyNames = require('./dotenvPrivateKeyNames') 2 | const guessPrivateKeyFilename = require('./guessPrivateKeyFilename') 3 | 4 | const TYPE_ENV_FILE = 'envFile' 5 | const TYPE_ENV_VAULT_FILE = 'envVaultFile' 6 | const DEFAULT_ENVS = [{ type: TYPE_ENV_FILE, value: '.env' }] 7 | const DEFAULT_ENV_VAULTS = [{ type: TYPE_ENV_VAULT_FILE, value: '.env.vault' }] 8 | 9 | function determineEnvsFromDotenvPrivateKey (privateKeyNames) { 10 | const envs = [] 11 | 12 | for (const privateKeyName of privateKeyNames) { 13 | const filename = guessPrivateKeyFilename(privateKeyName) 14 | envs.push({ type: TYPE_ENV_FILE, value: filename }) 15 | } 16 | 17 | return envs 18 | } 19 | 20 | function determineEnvs (envs = [], processEnv, DOTENV_KEY = '') { 21 | const privateKeyNames = dotenvPrivateKeyNames(processEnv) 22 | if (!envs || envs.length <= 0) { 23 | // if process.env.DOTENV_PRIVATE_KEY or process.env.DOTENV_PRIVATE_KEY_${environment} is set, assume inline encryption methodology 24 | if (privateKeyNames.length > 0) { 25 | return determineEnvsFromDotenvPrivateKey(privateKeyNames) 26 | } 27 | 28 | if (DOTENV_KEY.length > 0) { 29 | // if DOTENV_KEY is set then default to look for .env.vault file 30 | return DEFAULT_ENV_VAULTS 31 | } else { 32 | return DEFAULT_ENVS // default to .env file expectation 33 | } 34 | } else { 35 | let fileAlreadySpecified = false // can be .env or .env.vault type 36 | 37 | for (const env of envs) { 38 | // if DOTENV_KEY set then we are checking if a .env.vault file is already specified 39 | if (DOTENV_KEY.length > 0 && env.type === TYPE_ENV_VAULT_FILE) { 40 | fileAlreadySpecified = true 41 | } 42 | 43 | // if DOTENV_KEY not set then we are checking if a .env file is already specified 44 | if (DOTENV_KEY.length <= 0 && env.type === TYPE_ENV_FILE) { 45 | fileAlreadySpecified = true 46 | } 47 | } 48 | 49 | // return early since envs array objects already contain 1 .env.vault or .env file 50 | if (fileAlreadySpecified) { 51 | return envs 52 | } 53 | 54 | // no .env.vault or .env file specified as a flag so we assume either .env.vault (if dotenv key is set) or a .env file 55 | if (DOTENV_KEY.length > 0) { 56 | // if DOTENV_KEY is set then default to look for .env.vault file 57 | return [...DEFAULT_ENV_VAULTS, ...envs] 58 | } else { 59 | // if no DOTENV_KEY then default to look for .env file 60 | return [...DEFAULT_ENVS, ...envs] 61 | } 62 | } 63 | } 64 | 65 | module.exports = determineEnvs 66 | -------------------------------------------------------------------------------- /src/lib/helpers/dotenvOptionPaths.js: -------------------------------------------------------------------------------- 1 | const resolveHome = require('./resolveHome') 2 | 3 | function dotenvOptionPaths (options) { 4 | let optionPaths = [] 5 | 6 | if (options && options.path) { 7 | if (!Array.isArray(options.path)) { 8 | optionPaths = [resolveHome(options.path)] 9 | } else { 10 | optionPaths = [] // reset default 11 | 12 | for (const filepath of options.path) { 13 | optionPaths.push(resolveHome(filepath)) 14 | } 15 | } 16 | } 17 | 18 | return optionPaths 19 | } 20 | 21 | module.exports = dotenvOptionPaths 22 | -------------------------------------------------------------------------------- /src/lib/helpers/dotenvParse.js: -------------------------------------------------------------------------------- 1 | // historical dotenv.parse - https://github.com/motdotla/dotenv) 2 | const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg 3 | 4 | function dotenvParse (src, skipExpandForDoubleQuotes = false, skipConvertingWindowsNewlines = false, collectAllValues = false) { 5 | const obj = {} 6 | 7 | // Convert buffer to string 8 | let lines = src.toString() 9 | 10 | // Convert line breaks to same format 11 | if (!skipConvertingWindowsNewlines) { 12 | lines = lines.replace(/\r\n?/mg, '\n') 13 | } 14 | 15 | let match 16 | while ((match = LINE.exec(lines)) != null) { 17 | const key = match[1] 18 | 19 | // Default undefined or null to empty string 20 | let value = (match[2] || '') 21 | 22 | // Remove whitespace 23 | value = value.trim() 24 | 25 | // Check if double quoted 26 | const maybeQuote = value[0] 27 | 28 | // Remove surrounding quotes 29 | value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2') 30 | 31 | // Expand newlines if double quoted 32 | if (maybeQuote === '"' && !skipExpandForDoubleQuotes) { 33 | value = value.replace(/\\n/g, '\n') // newline 34 | value = value.replace(/\\r/g, '\r') // carriage return 35 | value = value.replace(/\\t/g, '\t') // tabs 36 | } 37 | 38 | if (collectAllValues) { 39 | // handle scenario where user mistakenly includes plaintext duplicate in .env: 40 | // 41 | // # .env 42 | // HELLO="World" 43 | // HELLO="enrypted:1234" 44 | obj[key] = obj[key] || [] 45 | obj[key].push(value) 46 | } else { 47 | // Add to object 48 | obj[key] = value 49 | } 50 | } 51 | 52 | return obj 53 | } 54 | 55 | module.exports = dotenvParse 56 | -------------------------------------------------------------------------------- /src/lib/helpers/dotenvPrivateKeyNames.js: -------------------------------------------------------------------------------- 1 | const PRIVATE_KEY_NAME_SCHEMA = 'DOTENV_PRIVATE_KEY' 2 | 3 | function dotenvPrivateKeyNames (processEnv) { 4 | return Object.keys(processEnv).filter(key => key.startsWith(PRIVATE_KEY_NAME_SCHEMA)) 5 | } 6 | 7 | module.exports = dotenvPrivateKeyNames 8 | -------------------------------------------------------------------------------- /src/lib/helpers/encrypt.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | const parseEncryptionKeyFromDotenvKey = require('./parseEncryptionKeyFromDotenvKey') 4 | 5 | const NONCE_BYTES = 12 6 | 7 | function encrypt (raw, dotenvKey) { 8 | const key = parseEncryptionKeyFromDotenvKey(dotenvKey) 9 | 10 | // set up nonce 11 | const nonce = crypto.randomBytes(NONCE_BYTES) 12 | 13 | // set up cipher 14 | const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce) 15 | 16 | // generate ciphertext 17 | let ciphertext = '' 18 | ciphertext += cipher.update(raw, 'utf8', 'hex') 19 | ciphertext += cipher.final('hex') 20 | ciphertext += cipher.getAuthTag().toString('hex') 21 | 22 | // prepend nonce 23 | ciphertext = nonce.toString('hex') + ciphertext 24 | 25 | // base64 encode output 26 | return Buffer.from(ciphertext, 'hex').toString('base64') 27 | } 28 | 29 | module.exports = encrypt 30 | -------------------------------------------------------------------------------- /src/lib/helpers/encryptValue.js: -------------------------------------------------------------------------------- 1 | const { encrypt } = require('eciesjs') 2 | 3 | const PREFIX = 'encrypted:' 4 | 5 | function encryptValue (value, publicKey) { 6 | const ciphertext = encrypt(publicKey, Buffer.from(value)) 7 | const encoded = Buffer.from(ciphertext, 'hex').toString('base64') // base64 encode ciphertext 8 | 9 | return `${PREFIX}${encoded}` 10 | } 11 | 12 | module.exports = encryptValue 13 | -------------------------------------------------------------------------------- /src/lib/helpers/errors.js: -------------------------------------------------------------------------------- 1 | const truncate = require('./truncate') 2 | 3 | class Errors { 4 | constructor (options = {}) { 5 | this.filepath = options.filepath 6 | this.envFilepath = options.envFilepath 7 | 8 | this.key = options.key 9 | this.privateKey = options.privateKey 10 | this.privateKeyName = options.privateKeyName 11 | this.command = options.command 12 | 13 | this.message = options.message 14 | } 15 | 16 | missingEnvFile () { 17 | const code = 'MISSING_ENV_FILE' 18 | const message = `[${code}] missing ${this.envFilepath} file (${this.filepath})` 19 | const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/484` 20 | 21 | const e = new Error(message) 22 | e.code = code 23 | e.help = help 24 | return e 25 | } 26 | 27 | missingKey () { 28 | const code = 'MISSING_KEY' 29 | const message = `[${code}] missing ${this.key} key` 30 | 31 | const e = new Error(message) 32 | e.code = code 33 | return e 34 | } 35 | 36 | missingPrivateKey () { 37 | const code = 'MISSING_PRIVATE_KEY' 38 | const message = `[${code}] could not decrypt ${this.key} using private key '${this.privateKeyName}=${truncate(this.privateKey)}'` 39 | const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/464` 40 | 41 | const e = new Error(message) 42 | e.code = code 43 | e.help = help 44 | return e 45 | } 46 | 47 | invalidPrivateKey () { 48 | const code = 'INVALID_PRIVATE_KEY' 49 | const message = `[${code}] could not decrypt ${this.key} using private key '${this.privateKeyName}=${truncate(this.privateKey)}'` 50 | const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/465` 51 | 52 | const e = new Error(message) 53 | e.code = code 54 | e.help = help 55 | return e 56 | } 57 | 58 | looksWrongPrivateKey () { 59 | const code = 'WRONG_PRIVATE_KEY' 60 | const message = `[${code}] could not decrypt ${this.key} using private key '${this.privateKeyName}=${truncate(this.privateKey)}'` 61 | const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/466` 62 | 63 | const e = new Error(message) 64 | e.code = code 65 | e.help = help 66 | return e 67 | } 68 | 69 | malformedEncryptedData () { 70 | const code = 'MALFORMED_ENCRYPTED_DATA' 71 | const message = `[${code}] could not decrypt ${this.key} because encrypted data appears malformed` 72 | const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/467` 73 | 74 | const e = new Error(message) 75 | e.code = code 76 | e.help = help 77 | return e 78 | } 79 | 80 | decryptionFailed () { 81 | const code = 'DECRYPTION_FAILED' 82 | const message = this.message 83 | 84 | const e = new Error(message) 85 | e.code = code 86 | return e 87 | } 88 | 89 | commandSubstitutionFailed () { 90 | const code = 'COMMAND_SUBSTITUTION_FAILED' 91 | const message = `[${code}] could not eval ${this.key} containing command '${this.command}': ${this.message}` 92 | const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/532` 93 | 94 | const e = new Error(message) 95 | e.code = code 96 | e.help = help 97 | return e 98 | } 99 | 100 | dangerousDependencyHoist () { 101 | const code = 'DANGEROUS_DEPENDENCY_HOIST' 102 | const message = `[${code}] your environment has hoisted an incompatible version of a dotenvx dependency: ${this.message}` 103 | const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/622` 104 | 105 | const e = new Error(message) 106 | e.code = code 107 | e.help = help 108 | return e 109 | } 110 | } 111 | 112 | module.exports = Errors 113 | -------------------------------------------------------------------------------- /src/lib/helpers/escape.js: -------------------------------------------------------------------------------- 1 | function escape (value) { 2 | return JSON.stringify(value) 3 | } 4 | 5 | module.exports = escape 6 | -------------------------------------------------------------------------------- /src/lib/helpers/escapeDollarSigns.js: -------------------------------------------------------------------------------- 1 | function escapeDollarSigns (str) { 2 | return str.replace(/\$/g, '$$') 3 | } 4 | 5 | module.exports = escapeDollarSigns 6 | -------------------------------------------------------------------------------- /src/lib/helpers/escapeForRegex.js: -------------------------------------------------------------------------------- 1 | function escapeForRegex (str) { 2 | return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\amp;').replace(/-/g, '\\x2d') 3 | } 4 | 5 | module.exports = escapeForRegex 6 | -------------------------------------------------------------------------------- /src/lib/helpers/evalKeyValue.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | const chomp = require('./chomp') 3 | const Errors = require('./errors') 4 | 5 | function evalKeyValue (key, value, processEnv, runningParsed) { 6 | // Match everything between the outermost $() using a regex with non-capturing groups 7 | const matches = value.match(/\$\(([^)]+(?:\)[^(]*)*)\)/g) || [] 8 | return matches.reduce((newValue, match) => { 9 | const command = match.slice(2, -1) // Extract command by removing $() wrapper 10 | let result 11 | 12 | try { 13 | result = execSync(command, { env: { ...processEnv, ...runningParsed } }).toString() // execute command (including runningParsed) 14 | } catch (e) { 15 | throw new Errors({ key, command, message: e.message.trim() }).commandSubstitutionFailed() 16 | } 17 | 18 | result = chomp(result) // chomp it 19 | return newValue.replace(match, result) // Replace match with result 20 | }, value) 21 | } 22 | 23 | module.exports = evalKeyValue 24 | -------------------------------------------------------------------------------- /src/lib/helpers/execute.js: -------------------------------------------------------------------------------- 1 | const execa = require('execa') 2 | /* c8 ignore start */ 3 | const pkgArgs = process.pkg ? { PKG_EXECPATH: '' } : {} 4 | /* c8 ignore stop */ 5 | 6 | const execute = { 7 | execa (command, args, options) { 8 | return execa(command, args, { ...options, env: { ...options.env, ...pkgArgs } }) 9 | } 10 | } 11 | 12 | module.exports = execute 13 | -------------------------------------------------------------------------------- /src/lib/helpers/executeCommand.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const which = require('which') 3 | const execute = require('./../../lib/helpers/execute') 4 | const { logger } = require('./../../shared/logger') 5 | 6 | async function executeCommand (commandArgs, env) { 7 | const signals = [ 8 | 'SIGHUP', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', 9 | 'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2' 10 | ] 11 | 12 | logger.debug(`executing process command [${commandArgs.join(' ')}]`) 13 | 14 | let child 15 | let signalSent 16 | 17 | /* c8 ignore start */ 18 | const sigintHandler = () => { 19 | logger.debug('received SIGINT') 20 | logger.debug('checking command process') 21 | logger.debug(child) 22 | 23 | if (child) { 24 | logger.debug('sending SIGINT to command process') 25 | signalSent = 'SIGINT' 26 | child.kill('SIGINT') // Send SIGINT to the command process 27 | } else { 28 | logger.debug('no command process to send SIGINT to') 29 | } 30 | } 31 | 32 | const sigtermHandler = () => { 33 | logger.debug('received SIGTERM') 34 | logger.debug('checking command process') 35 | logger.debug(child) 36 | 37 | if (child) { 38 | logger.debug('sending SIGTERM to command process') 39 | signalSent = 'SIGTERM' 40 | child.kill('SIGTERM') // Send SIGTERM to the command process 41 | } else { 42 | logger.debug('no command process to send SIGTERM to') 43 | } 44 | } 45 | 46 | const handleOtherSignal = (signal) => { 47 | logger.debug(`received ${signal}`) 48 | child.kill(signal) 49 | } 50 | /* c8 ignore stop */ 51 | 52 | try { 53 | // ensure the first command is expanded 54 | try { 55 | commandArgs[0] = path.resolve(which.sync(`${commandArgs[0]}`)) 56 | logger.debug(`expanding process command to [${commandArgs.join(' ')}]`) 57 | } catch (e) { 58 | logger.debug(`could not expand process command. using [${commandArgs.join(' ')}]`) 59 | } 60 | 61 | // expand any other commands that follow a -- 62 | let expandNext = false 63 | for (let i = 0; i < commandArgs.length; i++) { 64 | if (commandArgs[i] === '--') { 65 | expandNext = true 66 | } else if (expandNext) { 67 | try { 68 | commandArgs[i] = path.resolve(which.sync(`${commandArgs[i]}`)) 69 | logger.debug(`expanding process command to [${commandArgs.join(' ')}]`) 70 | } catch (e) { 71 | logger.debug(`could not expand process command. using [${commandArgs.join(' ')}]`) 72 | } 73 | expandNext = false 74 | } 75 | } 76 | 77 | child = execute.execa(commandArgs[0], commandArgs.slice(1), { 78 | stdio: 'inherit', 79 | env: { ...process.env, ...env } 80 | }) 81 | 82 | process.on('SIGINT', sigintHandler) 83 | process.on('SIGTERM', sigtermHandler) 84 | 85 | signals.forEach(signal => { 86 | process.on(signal, () => handleOtherSignal(signal)) 87 | }) 88 | 89 | // Wait for the command process to finish 90 | const { exitCode } = await child 91 | 92 | if (exitCode !== 0) { 93 | logger.debug(`received exitCode ${exitCode}`) 94 | throw new Error(`Command exited with exit code ${exitCode}`) 95 | } 96 | } catch (error) { 97 | // no color on these errors as they can be standard errors for things like jest exiting with exitCode 1 for a single failed test. 98 | if (!['SIGINT', 'SIGTERM'].includes(signalSent || error.signal)) { 99 | if (error.code === 'ENOENT') { 100 | logger.error(`Unknown command: ${error.command}`) 101 | } else { 102 | logger.error(error.message) 103 | } 104 | } 105 | 106 | // Exit with the error code from the command process, or 1 if unavailable 107 | process.exit(error.exitCode || 1) 108 | } finally { 109 | // Clean up: Remove the SIGINT handler 110 | process.removeListener('SIGINT', sigintHandler) 111 | // Clean up: Remove the SIGTERM handler 112 | process.removeListener('SIGTERM', sigtermHandler) 113 | } 114 | } 115 | 116 | module.exports = executeCommand 117 | -------------------------------------------------------------------------------- /src/lib/helpers/executeDynamic.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const childProcess = require('child_process') 3 | const { logger } = require('../../shared/logger') 4 | 5 | function executeDynamic (program, command, rawArgs) { 6 | if (!command) { 7 | program.outputHelp() 8 | process.exit(1) 9 | return 10 | } 11 | 12 | // construct the full command line manually including flags 13 | const commandIndex = rawArgs.indexOf(command) 14 | const forwardedArgs = rawArgs.slice(commandIndex + 1) 15 | 16 | logger.debug(`command: ${command}`) 17 | logger.debug(`args: ${JSON.stringify(forwardedArgs)}`) 18 | 19 | const binPath = path.join(process.cwd(), 'node_modules', '.bin') 20 | const newPath = `${binPath}:${process.env.PATH}` 21 | const env = { ...process.env, PATH: newPath } 22 | 23 | const result = childProcess.spawnSync(`dotenvx-${command}`, forwardedArgs, { stdio: 'inherit', env }) 24 | if (result.error) { 25 | if (command === 'pro') { 26 | const pro = `_______________________________________________________________ 27 | | | 28 | | For small and medium businesses | 29 | | | 30 | | | | | | | 31 | | __| | ___ | |_ ___ _ ____ ____ __ _ __ _ __ ___ | 32 | | / _\` |/ _ \\| __/ _ \\ '_ \\ \\ / /\\ \\/ / | '_ \\| '__/ _ \\ | 33 | | | (_| | (_) | || __/ | | \\ V / > < | |_) | | | (_) | | 34 | | \\__,_|\\___/ \\__\\___|_| |_|\\_/ /_/\\_\\ | .__/|_| \\___/ | 35 | | | | | 36 | | |_| | 37 | | ## learn more on dotenvx 🟨 | 38 | | | 39 | | >> https://dotenvx.com/pricing | 40 | | | 41 | | ## subscribe on github to be notified 📣 | 42 | | | 43 | | >> https://github.com/dotenvx/dotenvx/issues/259 | 44 | | | 45 | | ----------------------------------------------------------- | 46 | | - thank you for using dotenvx! - @motdotla | 47 | |_____________________________________________________________|` 48 | 49 | console.log(pro) 50 | console.log('') 51 | logger.warn(`[INSTALLATION_NEEDED] install dotenvx-${command} to use [dotenvx ${command}] commands 🏆`) 52 | logger.help('? see installation instructions [https://github.com/dotenvx/dotenvx-pro]') 53 | } else { 54 | logger.info(`error: unknown command '${command}'`) 55 | } 56 | } 57 | 58 | if (result.status !== 0) { 59 | process.exit(result.status) 60 | } 61 | } 62 | 63 | module.exports = executeDynamic 64 | -------------------------------------------------------------------------------- /src/lib/helpers/executeExtension.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const childProcess = require('child_process') 3 | const { logger } = require('../../shared/logger') 4 | 5 | function executeExtension (ext, command, rawArgs) { 6 | if (!command) { 7 | ext.outputHelp() 8 | process.exit(0) 9 | return 10 | } 11 | 12 | // construct the full command line manually including flags 13 | const commandIndex = rawArgs.indexOf(command) 14 | const forwardedArgs = rawArgs.slice(commandIndex + 1) 15 | 16 | logger.debug(`command: ${command}`) 17 | logger.debug(`args: ${JSON.stringify(forwardedArgs)}`) 18 | 19 | const binPath = path.join(process.cwd(), 'node_modules', '.bin') 20 | const newPath = `${binPath}:${process.env.PATH}` 21 | const env = { ...process.env, PATH: newPath } 22 | 23 | const result = childProcess.spawnSync(`dotenvx-ext-${command}`, forwardedArgs, { stdio: 'inherit', env }) 24 | if (result.error) { 25 | // list known extension here for convenience to the user 26 | if (['vault', 'hub'].includes(command)) { 27 | logger.warn(`[INSTALLATION_NEEDED] install dotenvx-ext-${command} to use [dotenvx ext ${command}] commands`) 28 | logger.help('? see installation instructions [https://github.com/dotenvx/dotenvx-ext-vault]') 29 | } else { 30 | logger.info(`error: unknown command '${command}'`) 31 | } 32 | } 33 | 34 | if (result.status !== 0) { 35 | process.exit(result.status) 36 | } 37 | } 38 | 39 | module.exports = executeExtension 40 | -------------------------------------------------------------------------------- /src/lib/helpers/findEnvFiles.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./fsx') 2 | 3 | const RESERVED_ENV_FILES = ['.env.vault', '.env.project', '.env.keys', '.env.me', '.env.x', '.env.example'] 4 | 5 | function findEnvFiles (directory) { 6 | try { 7 | const files = fsx.readdirSync(directory) 8 | const envFiles = files.filter(file => 9 | file.startsWith('.env') && 10 | !file.endsWith('.previous') && 11 | !RESERVED_ENV_FILES.includes(file) 12 | ) 13 | 14 | return envFiles 15 | } catch (e) { 16 | if (e.code === 'ENOENT') { 17 | const error = new Error(`missing directory (${directory})`) 18 | error.code = 'MISSING_DIRECTORY' 19 | 20 | throw error 21 | } else { 22 | throw e 23 | } 24 | } 25 | } 26 | 27 | module.exports = findEnvFiles 28 | -------------------------------------------------------------------------------- /src/lib/helpers/findPrivateKey.js: -------------------------------------------------------------------------------- 1 | // helpers 2 | const guessPrivateKeyName = require('./guessPrivateKeyName') 3 | const ProKeypair = require('./proKeypair') 4 | 5 | // services 6 | const Keypair = require('./../services/keypair') 7 | 8 | function findPrivateKey (envFilepath, envKeysFilepath = null) { 9 | // use path/to/.env.${environment} to generate privateKeyName 10 | const privateKeyName = guessPrivateKeyName(envFilepath) 11 | 12 | const proKeypairs = new ProKeypair(envFilepath).run() // TODO: implement custom envKeysFilepath 13 | const keypairs = new Keypair(envFilepath, envKeysFilepath).run() 14 | 15 | return proKeypairs[privateKeyName] || keypairs[privateKeyName] 16 | } 17 | 18 | module.exports = { findPrivateKey } 19 | -------------------------------------------------------------------------------- /src/lib/helpers/findPublicKey.js: -------------------------------------------------------------------------------- 1 | // helpers 2 | const guessPublicKeyName = require('./guessPublicKeyName') 3 | const ProKeypair = require('./proKeypair') 4 | 5 | // services 6 | const Keypair = require('./../services/keypair') 7 | 8 | function findPublicKey (envFilepath) { 9 | const publicKeyName = guessPublicKeyName(envFilepath) 10 | 11 | const proKeypairs = new ProKeypair(envFilepath).run() 12 | const keypairs = new Keypair(envFilepath).run() 13 | 14 | return proKeypairs[publicKeyName] || keypairs[publicKeyName] 15 | } 16 | 17 | module.exports = findPublicKey 18 | -------------------------------------------------------------------------------- /src/lib/helpers/fsx.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const ENCODING = 'utf8' 4 | 5 | function readFileX (filepath, encoding = null) { 6 | if (!encoding) { 7 | encoding = ENCODING 8 | } 9 | 10 | return fs.readFileSync(filepath, encoding) // utf8 default so it returns a string 11 | } 12 | 13 | function writeFileX (filepath, str) { 14 | return fs.writeFileSync(filepath, str, ENCODING) // utf8 always 15 | } 16 | 17 | const fsx = { 18 | chmodSync: fs.chmodSync, 19 | existsSync: fs.existsSync, 20 | readdirSync: fs.readdirSync, 21 | readFileSync: fs.readFileSync, 22 | writeFileSync: fs.writeFileSync, 23 | appendFileSync: fs.appendFileSync, 24 | 25 | // fsx special commands 26 | readFileX, 27 | writeFileX 28 | } 29 | 30 | module.exports = fsx 31 | -------------------------------------------------------------------------------- /src/lib/helpers/getCommanderVersion.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | function getCommanderVersion () { 5 | const commanderMain = require.resolve('commander') 6 | const pkgPath = path.join(commanderMain, '..', 'package.json') 7 | return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version 8 | } 9 | 10 | module.exports = getCommanderVersion 11 | -------------------------------------------------------------------------------- /src/lib/helpers/guessEnvironment.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | function guessEnvironment (filepath) { 4 | const filename = path.basename(filepath) 5 | 6 | const parts = filename.split('.') 7 | const possibleEnvironmentList = [...parts.slice(2)] 8 | 9 | if (possibleEnvironmentList.length === 0) { 10 | // handle .env1 -> development1 11 | const environment = filename.replace('.env', 'development') 12 | 13 | return environment 14 | } 15 | 16 | if (possibleEnvironmentList.length === 1) { 17 | return possibleEnvironmentList[0] 18 | } 19 | 20 | if ( 21 | possibleEnvironmentList.length === 2 22 | ) { 23 | return possibleEnvironmentList.join('_') 24 | } 25 | 26 | return possibleEnvironmentList.slice(0, 2).join('_') 27 | } 28 | 29 | module.exports = guessEnvironment 30 | -------------------------------------------------------------------------------- /src/lib/helpers/guessPrivateKeyFilename.js: -------------------------------------------------------------------------------- 1 | const PREFIX = 'DOTENV_PRIVATE_KEY' 2 | 3 | function guessPrivateKeyFilename (privateKeyName) { 4 | // .env 5 | if (privateKeyName === PREFIX) { 6 | return '.env' 7 | } 8 | 9 | const filenameSuffix = privateKeyName.substring(`${PREFIX}_`.length).split('_').join('.').toLowerCase() 10 | // .env.ENVIRONMENT 11 | 12 | return `.env.${filenameSuffix}` 13 | } 14 | 15 | module.exports = guessPrivateKeyFilename 16 | -------------------------------------------------------------------------------- /src/lib/helpers/guessPrivateKeyName.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const guessEnvironment = require('./guessEnvironment') 3 | 4 | function guessPrivateKeyName (filepath) { 5 | const filename = path.basename(filepath).toLowerCase() 6 | 7 | // .env 8 | if (filename === '.env') { 9 | return 'DOTENV_PRIVATE_KEY' 10 | } 11 | 12 | // .env.ENVIRONMENT 13 | const environment = guessEnvironment(filename) 14 | 15 | return `DOTENV_PRIVATE_KEY_${environment.toUpperCase()}` 16 | } 17 | 18 | module.exports = guessPrivateKeyName 19 | -------------------------------------------------------------------------------- /src/lib/helpers/guessPublicKeyName.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const guessEnvironment = require('./guessEnvironment') 3 | 4 | function guessPublicKeyName (filepath) { 5 | const filename = path.basename(filepath).toLowerCase() 6 | 7 | // .env 8 | if (filename === '.env') { 9 | return 'DOTENV_PUBLIC_KEY' 10 | } 11 | 12 | // .env.ENVIRONMENT 13 | const environment = guessEnvironment(filename) 14 | 15 | return `DOTENV_PUBLIC_KEY_${environment.toUpperCase()}` 16 | } 17 | 18 | module.exports = guessPublicKeyName 19 | -------------------------------------------------------------------------------- /src/lib/helpers/installPrecommitHook.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./fsx') 2 | const path = require('path') 3 | 4 | const HOOK_SCRIPT = `#!/bin/sh 5 | 6 | if command -v dotenvx 2>&1 >/dev/null 7 | then 8 | dotenvx ext precommit 9 | elif npx dotenvx -V >/dev/null 2>&1 10 | then 11 | npx dotenvx ext precommit 12 | else 13 | echo "[dotenvx][precommit] 'dotenvx' command not found" 14 | echo "[dotenvx][precommit] ? install it with [curl -fsS https://dotenvx.sh | sh]" 15 | echo "[dotenvx][precommit] ? other install options [https://dotenvx.com/docs/install]" 16 | exit 1 17 | fi 18 | ` 19 | 20 | class InstallPrecommitHook { 21 | constructor () { 22 | this.hookPath = path.join('.git', 'hooks', 'pre-commit') 23 | } 24 | 25 | run () { 26 | let successMessage 27 | 28 | try { 29 | // Check if the pre-commit file already exists 30 | if (this._exists()) { 31 | // Check if 'dotenvx precommit' already exists in the file 32 | if (this._currentHook().includes('dotenvx ext precommit')) { 33 | // do nothing 34 | successMessage = `dotenvx ext precommit exists [${this.hookPath}]` 35 | } else { 36 | this._appendHook() 37 | successMessage = `dotenvx ext precommit appended [${this.hookPath}]` 38 | } 39 | } else { 40 | this._createHook() 41 | successMessage = `dotenvx ext precommit installed [${this.hookPath}]` 42 | } 43 | 44 | return { 45 | successMessage 46 | } 47 | } catch (err) { 48 | const error = new Error(`failed to modify pre-commit hook: ${err.message}`) 49 | throw error 50 | } 51 | } 52 | 53 | _exists () { 54 | return fsx.existsSync(this.hookPath) 55 | } 56 | 57 | _currentHook () { 58 | return fsx.readFileX(this.hookPath) 59 | } 60 | 61 | _createHook () { 62 | // If the pre-commit file doesn't exist, create a new one with the hookScript 63 | fsx.writeFileX(this.hookPath, HOOK_SCRIPT) 64 | fsx.chmodSync(this.hookPath, '755') // Make the file executable 65 | } 66 | 67 | _appendHook () { 68 | // Append 'dotenvx precommit' to the existing file 69 | fsx.appendFileSync(this.hookPath, '\n' + HOOK_SCRIPT) 70 | } 71 | } 72 | 73 | module.exports = InstallPrecommitHook 74 | -------------------------------------------------------------------------------- /src/lib/helpers/isEncrypted.js: -------------------------------------------------------------------------------- 1 | const ENCRYPTION_PATTERN = /^encrypted:.+/ 2 | 3 | function isEncrypted (value) { 4 | return ENCRYPTION_PATTERN.test(value) 5 | } 6 | 7 | module.exports = isEncrypted 8 | -------------------------------------------------------------------------------- /src/lib/helpers/isFullyEncrypted.js: -------------------------------------------------------------------------------- 1 | const dotenvParse = require('./dotenvParse') 2 | const isEncrypted = require('./isEncrypted') 3 | const isPublicKey = require('./isPublicKey') 4 | 5 | function isFullyEncrypted (src) { 6 | const parsed = dotenvParse(src, false, false, true) // collect all values 7 | 8 | for (const [key, values] of Object.entries(parsed)) { 9 | // handle scenario where user mistakenly includes plaintext duplicate in .env: 10 | // 11 | // # .env 12 | // HELLO="World" 13 | // HELLO="enrypted:1234" 14 | // 15 | // key => [value1, ...] 16 | for (const value of values) { 17 | const result = isEncrypted(value) || isPublicKey(key, value) 18 | if (!result) { 19 | return false 20 | } 21 | } 22 | } 23 | 24 | return true 25 | } 26 | 27 | module.exports = isFullyEncrypted 28 | -------------------------------------------------------------------------------- /src/lib/helpers/isIgnoringDotenvKeys.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./fsx') 2 | const ignore = require('ignore') 3 | 4 | function isIgnoringDotenvKeys () { 5 | if (!fsx.existsSync('.gitignore')) { 6 | return false 7 | } 8 | 9 | const gitignore = fsx.readFileX('.gitignore') 10 | const ig = ignore(gitignore).add(gitignore) 11 | 12 | if (!ig.ignores('.env.keys')) { 13 | return false 14 | } 15 | 16 | return true 17 | } 18 | 19 | module.exports = isIgnoringDotenvKeys 20 | -------------------------------------------------------------------------------- /src/lib/helpers/isPublicKey.js: -------------------------------------------------------------------------------- 1 | const PUBLIC_KEY_PATTERN = /^DOTENV_PUBLIC_KEY/ 2 | 3 | function isPublicKey (key, value) { 4 | return PUBLIC_KEY_PATTERN.test(key) 5 | } 6 | 7 | module.exports = isPublicKey 8 | -------------------------------------------------------------------------------- /src/lib/helpers/keypair.js: -------------------------------------------------------------------------------- 1 | const { PrivateKey } = require('eciesjs') 2 | 3 | function keypair (existingPrivateKey) { 4 | let kp 5 | 6 | if (existingPrivateKey) { 7 | kp = new PrivateKey(Buffer.from(existingPrivateKey, 'hex')) 8 | } else { 9 | kp = new PrivateKey() 10 | } 11 | 12 | const publicKey = kp.publicKey.toHex() 13 | const privateKey = kp.secret.toString('hex') 14 | 15 | return { 16 | publicKey, 17 | privateKey 18 | } 19 | } 20 | 21 | module.exports = keypair 22 | -------------------------------------------------------------------------------- /src/lib/helpers/packageJson.js: -------------------------------------------------------------------------------- 1 | const { name, version, description } = require('../../../package.json') 2 | 3 | module.exports = { name, version, description } 4 | -------------------------------------------------------------------------------- /src/lib/helpers/parseEncryptionKeyFromDotenvKey.js: -------------------------------------------------------------------------------- 1 | function parseEncryptionKeyFromDotenvKey (dotenvKey) { 2 | // Parse DOTENV_KEY. Format is a URI 3 | let uri 4 | try { 5 | uri = new URL(dotenvKey) 6 | } catch (e) { 7 | throw new Error('INVALID_DOTENV_KEY: Incomplete format. It should be a dotenv uri. (dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development)') 8 | } 9 | 10 | // Get decrypt key 11 | const key = uri.password 12 | if (!key) { 13 | throw new Error('INVALID_DOTENV_KEY: Missing key part') 14 | } 15 | 16 | return Buffer.from(key.slice(-64), 'hex') 17 | } 18 | 19 | module.exports = parseEncryptionKeyFromDotenvKey 20 | -------------------------------------------------------------------------------- /src/lib/helpers/parseEnvironmentFromDotenvKey.js: -------------------------------------------------------------------------------- 1 | function parseEnvironmentFromDotenvKey (dotenvKey) { 2 | // Parse DOTENV_KEY. Format is a URI 3 | let uri 4 | try { 5 | uri = new URL(dotenvKey) 6 | } catch (e) { 7 | throw new Error(`INVALID_DOTENV_KEY: ${e.message}`) 8 | } 9 | 10 | // Get environment 11 | const environment = uri.searchParams.get('environment') 12 | if (!environment) { 13 | throw new Error('INVALID_DOTENV_KEY: Missing environment part') 14 | } 15 | 16 | return environment 17 | } 18 | 19 | module.exports = parseEnvironmentFromDotenvKey 20 | -------------------------------------------------------------------------------- /src/lib/helpers/pluralize.js: -------------------------------------------------------------------------------- 1 | function pluralize (word, count) { 2 | // simple pluralization: add 's' at the end 3 | if (count === 0 || count > 1) { 4 | return word + 's' 5 | } else { 6 | return word 7 | } 8 | } 9 | 10 | module.exports = pluralize 11 | -------------------------------------------------------------------------------- /src/lib/helpers/proKeypair.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const childProcess = require('child_process') 3 | 4 | const guessPrivateKeyName = require('./guessPrivateKeyName') 5 | const guessPublicKeyName = require('./guessPublicKeyName') 6 | 7 | class ProKeypair { 8 | constructor (envFilepath) { 9 | this.envFilepath = envFilepath 10 | } 11 | 12 | run () { 13 | let result = {} 14 | 15 | try { 16 | // if installed as sibling module 17 | const projectRoot = path.resolve(process.cwd()) 18 | const dotenvxProPath = require.resolve('@dotenvx/dotenvx-pro', { paths: [projectRoot] }) 19 | const { keypair } = require(dotenvxProPath) 20 | 21 | result = keypair(this.envFilepath) 22 | } catch (_e) { 23 | try { 24 | // if installed as binary cli 25 | const output = childProcess.execSync(`dotenvx-pro keypair -f ${this.envFilepath}`, { stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim() 26 | 27 | result = JSON.parse(output) 28 | } catch (_e) { 29 | const privateKeyName = guessPrivateKeyName(this.envFilepath) 30 | const publicKeyName = guessPublicKeyName(this.envFilepath) 31 | 32 | // match format of dotenvx-pro 33 | result[privateKeyName] = null 34 | result[publicKeyName] = null 35 | } 36 | } 37 | 38 | return result 39 | } 40 | } 41 | 42 | module.exports = ProKeypair 43 | -------------------------------------------------------------------------------- /src/lib/helpers/quotes.js: -------------------------------------------------------------------------------- 1 | const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg 2 | 3 | function quotes (src) { 4 | const obj = {} 5 | // Convert buffer to string 6 | let lines = src.toString() 7 | 8 | // Convert line breaks to same format 9 | lines = lines.replace(/\r\n?/mg, '\n') 10 | 11 | let match 12 | while ((match = LINE.exec(lines)) != null) { 13 | const key = match[1] 14 | 15 | // Default undefined or null to empty string 16 | let value = (match[2] || '') 17 | 18 | // Remove whitespace 19 | value = value.trim() 20 | 21 | // Check if double quoted 22 | const maybeQuote = value[0] 23 | 24 | value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2') 25 | 26 | if (maybeQuote === value[0]) { 27 | obj[key] = '' 28 | } else { 29 | obj[key] = maybeQuote 30 | } 31 | } 32 | 33 | return obj 34 | } 35 | 36 | module.exports = quotes 37 | -------------------------------------------------------------------------------- /src/lib/helpers/removeDynamicHelpSection.js: -------------------------------------------------------------------------------- 1 | // Remove Arguments section from help text. example: 2 | // Arguments: 3 | // command dynamic command 4 | // args dynamic command arguments 5 | 6 | function removeDynamicHelpSection (lines) { 7 | let argumentsHelpIndex 8 | for (let i = 0; i < lines.length; i++) { 9 | if (lines[i] === 'Arguments:') { 10 | argumentsHelpIndex = i 11 | break 12 | } 13 | } 14 | if (argumentsHelpIndex) { 15 | lines.splice(argumentsHelpIndex, 4) // remove Arguments and the following 3 lines 16 | } 17 | 18 | return lines 19 | } 20 | 21 | module.exports = removeDynamicHelpSection 22 | -------------------------------------------------------------------------------- /src/lib/helpers/removeOptionsHelpParts.js: -------------------------------------------------------------------------------- 1 | // Remove [options] from help text. example: 2 | 3 | function removeOptionsHelpParts (lines) { 4 | for (let i = 0; i < lines.length; i++) { 5 | lines[i] = lines[i].replace(' [options]', '') 6 | } 7 | 8 | return lines 9 | } 10 | 11 | module.exports = removeOptionsHelpParts 12 | -------------------------------------------------------------------------------- /src/lib/helpers/replace.js: -------------------------------------------------------------------------------- 1 | const quotes = require('./quotes') 2 | const dotenvParse = require('./dotenvParse') 3 | const escapeForRegex = require('./escapeForRegex') 4 | const escapeDollarSigns = require('./escapeDollarSigns') 5 | 6 | function replace (src, key, replaceValue) { 7 | let output 8 | let newPart = '' 9 | 10 | const parsed = dotenvParse(src, true, true) // skip expanding \n and skip converting \r\n 11 | const _quotes = quotes(src) 12 | if (Object.prototype.hasOwnProperty.call(parsed, key)) { 13 | const quote = _quotes[key] 14 | newPart += `${key}=${quote}${replaceValue}${quote}` 15 | 16 | const originalValue = parsed[key] 17 | const escapedOriginalValue = escapeForRegex(originalValue) 18 | 19 | // conditionally enforce end of line 20 | let enforceEndOfLine = '' 21 | if (escapedOriginalValue === '') { 22 | enforceEndOfLine = '#39; // EMPTY scenario 23 | 24 | // if empty quote and consecutive newlines 25 | const newlineMatch = src.match(new RegExp(`${key}\\s*=\\s*\n\n`, 'm')) // match any consecutive newline scenario for a blank value 26 | if (quote === '' && newlineMatch) { 27 | const newlineCount = (newlineMatch[0].match(/\n/g)).length - 1 28 | for (let i = 0; i < newlineCount; i++) { 29 | newPart += '\n' // re-append the extra newline to preserve user's format choice 30 | } 31 | } 32 | } 33 | 34 | const currentPart = new RegExp( 35 | '^' + // start of line 36 | '(\\s*)?' + // spaces 37 | '(export\\s+)?' + // export 38 | key + // KEY 39 | '\\s*=\\s*' + // spaces (KEY = value) 40 | '["\'`]?' + // open quote 41 | escapedOriginalValue + // escaped value 42 | '["\'`]?' + // close quote 43 | enforceEndOfLine 44 | , 45 | 'gm' // (g)lobal (m)ultiline 46 | ) 47 | 48 | const saferInput = escapeDollarSigns(newPart) // cleanse user inputted capture groups ($1, $2 etc) 49 | 50 | // $1 preserves spaces 51 | // $2 preserves export 52 | output = src.replace(currentPart, `$1$2${saferInput}`) 53 | } else { 54 | newPart += `${key}="${replaceValue}"` 55 | 56 | // append 57 | if (src.endsWith('\n')) { 58 | newPart = newPart + '\n' 59 | } else { 60 | newPart = '\n' + newPart 61 | } 62 | 63 | output = src + newPart 64 | } 65 | 66 | return output 67 | } 68 | 69 | module.exports = replace 70 | -------------------------------------------------------------------------------- /src/lib/helpers/resolveEscapeSequences.js: -------------------------------------------------------------------------------- 1 | function resolveEscapeSequences (value) { 2 | return value.replace(/\\\$/g, '#39;) 3 | } 4 | 5 | module.exports = resolveEscapeSequences 6 | -------------------------------------------------------------------------------- /src/lib/helpers/resolveHome.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const path = require('path') 3 | 4 | function resolveHome (filepath) { 5 | if (filepath[0] === '~') { 6 | return path.join(os.homedir(), filepath.slice(1)) 7 | } 8 | 9 | return filepath 10 | } 11 | 12 | module.exports = resolveHome 13 | -------------------------------------------------------------------------------- /src/lib/helpers/sleep.js: -------------------------------------------------------------------------------- 1 | function sleep (ms) { 2 | return new Promise(resolve => setTimeout(resolve, ms)) 3 | } 4 | 5 | module.exports = sleep 6 | -------------------------------------------------------------------------------- /src/lib/helpers/smartDotenvPrivateKey.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./fsx') 2 | const path = require('path') 3 | 4 | const PUBLIC_KEY_SCHEMA = 'DOTENV_PUBLIC_KEY' 5 | const PRIVATE_KEY_SCHEMA = 'DOTENV_PRIVATE_KEY' 6 | 7 | const dotenvParse = require('./dotenvParse') 8 | const guessPrivateKeyName = require('./guessPrivateKeyName') 9 | 10 | function searchProcessEnv (privateKeyName) { 11 | if (process.env[privateKeyName] && process.env[privateKeyName].length > 0) { 12 | return process.env[privateKeyName] 13 | } 14 | } 15 | 16 | function searchKeysFile (privateKeyName, envFilepath, envKeysFilepath = null) { 17 | let keysFilepath = path.resolve(path.dirname(envFilepath), '.env.keys') // typical scenario 18 | if (envKeysFilepath) { // user specified -fk flag 19 | keysFilepath = path.resolve(envKeysFilepath) 20 | } 21 | 22 | if (fsx.existsSync(keysFilepath)) { 23 | const keysSrc = fsx.readFileX(keysFilepath) 24 | const keysParsed = dotenvParse(keysSrc) 25 | 26 | if (keysParsed[privateKeyName] && keysParsed[privateKeyName].length > 0) { 27 | return keysParsed[privateKeyName] 28 | } 29 | } 30 | } 31 | 32 | function invertForPrivateKeyName (envFilepath) { 33 | if (!fsx.existsSync(envFilepath)) { 34 | return null 35 | } 36 | 37 | const envSrc = fsx.readFileX(envFilepath) 38 | const envParsed = dotenvParse(envSrc) 39 | 40 | let publicKeyName 41 | for (const keyName of Object.keys(envParsed)) { 42 | if (keyName === PUBLIC_KEY_SCHEMA || keyName.startsWith(PUBLIC_KEY_SCHEMA)) { 43 | publicKeyName = keyName // find DOTENV_PUBLIC_KEY* in filename 44 | } 45 | } 46 | 47 | if (publicKeyName) { 48 | return publicKeyName.replace(PUBLIC_KEY_SCHEMA, PRIVATE_KEY_SCHEMA) // return inverted (DOTENV_PUBLIC_KEY* -> DOTENV_PRIVATE_KEY*) if found 49 | } 50 | 51 | return null 52 | } 53 | 54 | function smartDotenvPrivateKey (envFilepath, envKeysFilepath = null) { 55 | let privateKey = null 56 | let privateKeyName = guessPrivateKeyName(envFilepath) // DOTENV_PRIVATE_KEY_${ENVIRONMENT} 57 | 58 | // 1. attempt process.env first 59 | privateKey = searchProcessEnv(privateKeyName) 60 | if (privateKey) { 61 | return privateKey 62 | } 63 | 64 | // 2. attempt .env.keys second (path/to/.env.keys) 65 | privateKey = searchKeysFile(privateKeyName, envFilepath, envKeysFilepath) 66 | if (privateKey) { 67 | return privateKey 68 | } 69 | 70 | // 3. attempt inverting `DOTENV_PUBLIC_KEY*` name inside file (unlocks custom filenames not matching .env.${ENVIRONMENT} pattern) 71 | privateKeyName = invertForPrivateKeyName(envFilepath) 72 | if (privateKeyName) { 73 | // 3.1 attempt process.env first 74 | privateKey = searchProcessEnv(privateKeyName) 75 | if (privateKey) { 76 | return privateKey 77 | } 78 | 79 | // 3.2. attempt .env.keys second (path/to/.env.keys) 80 | privateKey = searchKeysFile(privateKeyName, envFilepath, envKeysFilepath) 81 | if (privateKey) { 82 | return privateKey 83 | } 84 | } 85 | 86 | return null 87 | } 88 | 89 | module.exports = smartDotenvPrivateKey 90 | -------------------------------------------------------------------------------- /src/lib/helpers/smartDotenvPublicKey.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./fsx') 2 | const dotenvParse = require('./dotenvParse') 3 | 4 | const guessPublicKeyName = require('./guessPublicKeyName') 5 | 6 | function searchProcessEnv (publicKeyName) { 7 | if (process.env[publicKeyName] && process.env[publicKeyName].length > 0) { 8 | return process.env[publicKeyName] 9 | } 10 | } 11 | 12 | function searchEnvFile (publicKeyName, envFilepath) { 13 | if (fsx.existsSync(envFilepath)) { 14 | const keysSrc = fsx.readFileX(envFilepath) 15 | const keysParsed = dotenvParse(keysSrc) 16 | 17 | if (keysParsed[publicKeyName] && keysParsed[publicKeyName].length > 0) { 18 | return keysParsed[publicKeyName] 19 | } 20 | } 21 | } 22 | 23 | function smartDotenvPublicKey (envFilepath) { 24 | let publicKey = null 25 | const publicKeyName = guessPublicKeyName(envFilepath) // DOTENV_PUBLIC_KEY_${ENVIRONMENT} 26 | 27 | // 1. attempt process.env first 28 | publicKey = searchProcessEnv(publicKeyName) 29 | if (publicKey) { 30 | return publicKey 31 | } 32 | 33 | // 2. attempt .env.keys second (path/to/.env.keys) 34 | publicKey = searchEnvFile(publicKeyName, envFilepath) 35 | if (publicKey) { 36 | return publicKey 37 | } 38 | 39 | return null 40 | } 41 | 42 | module.exports = smartDotenvPublicKey 43 | -------------------------------------------------------------------------------- /src/lib/helpers/truncate.js: -------------------------------------------------------------------------------- 1 | function truncate (str, showChar = 7) { 2 | if (str && str.length > 0) { 3 | const visiblePart = str.slice(0, showChar) 4 | return visiblePart + '…' 5 | } else { 6 | return '' 7 | } 8 | } 9 | 10 | module.exports = truncate 11 | -------------------------------------------------------------------------------- /src/lib/services/genexample.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./../helpers/fsx') 2 | const path = require('path') 3 | 4 | const Errors = require('../helpers/errors') 5 | const findEnvFiles = require('../helpers/findEnvFiles') 6 | const replace = require('../helpers/replace') 7 | const dotenvParse = require('../helpers/dotenvParse') 8 | 9 | class Genexample { 10 | constructor (directory = '.', envFile) { 11 | this.directory = directory 12 | this.envFile = envFile || findEnvFiles(directory) 13 | 14 | this.exampleFilename = '.env.example' 15 | this.exampleFilepath = path.resolve(this.directory, this.exampleFilename) 16 | } 17 | 18 | run () { 19 | if (this.envFile.length < 1) { 20 | const code = 'MISSING_ENV_FILES' 21 | const message = 'no .env* files found' 22 | const help = '? add one with [echo "HELLO=World" > .env] and then run [dotenvx genexample]' 23 | 24 | const error = new Error(message) 25 | error.code = code 26 | error.help = help 27 | throw error 28 | } 29 | 30 | const keys = new Set() 31 | const addedKeys = new Set() 32 | const envFilepaths = this._envFilepaths() 33 | /** @type {Record<string, string>} */ 34 | const injected = {} 35 | /** @type {Record<string, string>} */ 36 | const preExisted = {} 37 | 38 | let exampleSrc = `# ${this.exampleFilename} - generated with dotenvx\n` 39 | 40 | for (const envFilepath of envFilepaths) { 41 | const filepath = path.resolve(this.directory, envFilepath) 42 | if (!fsx.existsSync(filepath)) { 43 | const error = new Errors({ envFilepath, filepath }).missingEnvFile() 44 | error.help = `? add it with [echo "HELLO=World" > ${envFilepath}] and then run [dotenvx genexample]` 45 | throw error 46 | } 47 | 48 | // get the original src 49 | let src = fsx.readFileX(filepath) 50 | const parsed = dotenvParse(src) 51 | for (const key in parsed) { 52 | // used later 53 | keys.add(key) 54 | 55 | // once newSrc is built write it out 56 | src = replace(src, key, '') // empty value 57 | } 58 | 59 | exampleSrc += `\n${src}` 60 | } 61 | 62 | if (!fsx.existsSync(this.exampleFilepath)) { 63 | // it doesn't exist so just write this first generated one 64 | // exampleSrc - already written to from the prior loop 65 | for (const key of [...keys]) { 66 | // every key is added since it's the first time generating .env.example 67 | addedKeys.add(key) 68 | 69 | injected[key] = '' 70 | } 71 | } else { 72 | // it already exists (which means the user might have it modified a way in which they prefer, so replace exampleSrc with their existing .env.example) 73 | exampleSrc = fsx.readFileX(this.exampleFilepath) 74 | 75 | const parsed = dotenvParse(exampleSrc) 76 | for (const key of [...keys]) { 77 | if (key in parsed) { 78 | preExisted[key] = parsed[key] 79 | } else { 80 | exampleSrc += `${key}=''\n` 81 | 82 | addedKeys.add(key) 83 | 84 | injected[key] = '' 85 | } 86 | } 87 | } 88 | 89 | return { 90 | envExampleFile: exampleSrc, 91 | envFile: this.envFile, 92 | exampleFilepath: this.exampleFilepath, 93 | addedKeys: [...addedKeys], 94 | injected, 95 | preExisted 96 | } 97 | } 98 | 99 | _envFilepaths () { 100 | if (!Array.isArray(this.envFile)) { 101 | return [this.envFile] 102 | } 103 | 104 | return this.envFile 105 | } 106 | } 107 | 108 | module.exports = Genexample 109 | -------------------------------------------------------------------------------- /src/lib/services/get.js: -------------------------------------------------------------------------------- 1 | const Run = require('./run') 2 | const Errors = require('./../helpers/errors') 3 | 4 | class Get { 5 | constructor (key, envs = [], overload = false, DOTENV_KEY = '', all = false, envKeysFilepath = null) { 6 | this.key = key 7 | this.envs = envs 8 | this.overload = overload 9 | this.DOTENV_KEY = DOTENV_KEY 10 | this.all = all 11 | this.envKeysFilepath = envKeysFilepath 12 | } 13 | 14 | run () { 15 | const processEnv = { ...process.env } 16 | const { processedEnvs } = new Run(this.envs, this.overload, this.DOTENV_KEY, processEnv, this.envKeysFilepath).run() 17 | 18 | const errors = [] 19 | for (const processedEnv of processedEnvs) { 20 | for (const error of processedEnv.errors) { 21 | errors.push(error) 22 | } 23 | } 24 | 25 | if (this.key) { 26 | const parsed = {} 27 | const value = processEnv[this.key] 28 | parsed[this.key] = value 29 | 30 | if (value === undefined) { 31 | errors.push(new Errors({ key: this.key }).missingKey()) 32 | } 33 | 34 | return { parsed, errors } 35 | } else { 36 | // if user wants to return ALL envs (even prior set on machine) 37 | if (this.all) { 38 | return { parsed: processEnv, errors } 39 | } 40 | 41 | // typical scenario - return only envs that were identified in the .env file 42 | // iterate over all processedEnvs.parsed and grab from processEnv 43 | /** @type {Record<string, string>} */ 44 | const parsed = {} 45 | for (const processedEnv of processedEnvs) { 46 | // parsed means we saw the key in a file or --env flag. this effectively filters out any preset machine envs - while still respecting complex evaluating, expansion, and overload. in other words, the value might be the machine value because the key was displayed in a .env file 47 | if (processedEnv.parsed) { 48 | for (const key of Object.keys(processedEnv.parsed)) { 49 | parsed[key] = processEnv[key] 50 | } 51 | } 52 | } 53 | 54 | return { parsed, errors } 55 | } 56 | } 57 | } 58 | 59 | module.exports = Get 60 | -------------------------------------------------------------------------------- /src/lib/services/keypair.js: -------------------------------------------------------------------------------- 1 | const guessPublicKeyName = require('./../helpers/guessPublicKeyName') 2 | const smartDotenvPublicKey = require('./../helpers/smartDotenvPublicKey') 3 | const guessPrivateKeyName = require('./../helpers/guessPrivateKeyName') 4 | const smartDotenvPrivateKey = require('./../helpers/smartDotenvPrivateKey') 5 | 6 | class Keypair { 7 | constructor (envFile = '.env', envKeysFilepath = null) { 8 | this.envFile = envFile 9 | this.envKeysFilepath = envKeysFilepath 10 | } 11 | 12 | run () { 13 | const out = {} 14 | 15 | const envFilepaths = this._envFilepaths() 16 | for (const envFilepath of envFilepaths) { 17 | // public key 18 | const publicKeyName = guessPublicKeyName(envFilepath) 19 | const publicKeyValue = smartDotenvPublicKey(envFilepath) 20 | out[publicKeyName] = publicKeyValue 21 | 22 | // private key 23 | const privateKeyName = guessPrivateKeyName(envFilepath) 24 | const privateKeyValue = smartDotenvPrivateKey(envFilepath, this.envKeysFilepath) 25 | 26 | out[privateKeyName] = privateKeyValue 27 | } 28 | 29 | return out 30 | } 31 | 32 | _envFilepaths () { 33 | if (!Array.isArray(this.envFile)) { 34 | return [this.envFile] 35 | } 36 | 37 | return this.envFile 38 | } 39 | } 40 | 41 | module.exports = Keypair 42 | -------------------------------------------------------------------------------- /src/lib/services/ls.js: -------------------------------------------------------------------------------- 1 | const { fdir: Fdir } = require('fdir') 2 | const path = require('path') 3 | const picomatch = require('picomatch') 4 | 5 | class Ls { 6 | constructor (directory = './', envFile = ['.env*'], excludeEnvFile = []) { 7 | this.ignore = ['node_modules/**', '.git/**'] 8 | 9 | this.cwd = path.resolve(directory) 10 | this.envFile = envFile 11 | this.excludeEnvFile = excludeEnvFile 12 | } 13 | 14 | run () { 15 | return this._filepaths() 16 | } 17 | 18 | _filepaths () { 19 | const exclude = picomatch(this._exclude()) 20 | const include = picomatch(this._patterns(), { 21 | ignore: this._exclude() 22 | }) 23 | 24 | return new Fdir() 25 | .withRelativePaths() 26 | .exclude((dir, path) => exclude(path)) 27 | .filter((path) => include(path)) 28 | .crawl(this.cwd) 29 | .sync() 30 | } 31 | 32 | _patterns () { 33 | if (!Array.isArray(this.envFile)) { 34 | return [`**/${this.envFile}`] 35 | } 36 | 37 | return this.envFile.map(part => `**/${part}`) 38 | } 39 | 40 | _excludePatterns () { 41 | if (!Array.isArray(this.excludeEnvFile)) { 42 | return [`**/${this.excludeEnvFile}`] 43 | } 44 | 45 | return this.excludeEnvFile.map(part => `**/${part}`) 46 | } 47 | 48 | _exclude () { 49 | if (this._excludePatterns().length > 0) { 50 | return this.ignore.concat(this._excludePatterns()) 51 | } else { 52 | return this.ignore 53 | } 54 | } 55 | } 56 | 57 | module.exports = Ls 58 | -------------------------------------------------------------------------------- /src/lib/services/prebuild.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const fsx = require('./../helpers/fsx') 3 | const path = require('path') 4 | const ignore = require('ignore') 5 | 6 | const Ls = require('../services/ls') 7 | 8 | const isFullyEncrypted = require('./../helpers/isFullyEncrypted') 9 | const packageJson = require('./../helpers/packageJson') 10 | const MISSING_DOCKERIGNORE = '.env.keys' // by default only ignore .env.keys. all other .env* files COULD be included - as long as they are encrypted 11 | 12 | class Prebuild { 13 | constructor (directory = './') { 14 | // args 15 | this.directory = directory 16 | 17 | this.excludeEnvFile = ['test/**', 'tests/**', 'spec/**', 'specs/**', 'pytest/**', 'test_suite/**'] 18 | } 19 | 20 | run () { 21 | let count = 0 22 | const warnings = [] 23 | let dockerignore = MISSING_DOCKERIGNORE 24 | 25 | // 1. check for .dockerignore file 26 | if (!fsx.existsSync('.dockerignore')) { 27 | const warning = new Error(`[dotenvx@${packageJson.version}][prebuild] .dockerignore missing`) 28 | warnings.push(warning) 29 | } else { 30 | dockerignore = fsx.readFileX('.dockerignore') 31 | } 32 | 33 | // 2. check .env* files against .dockerignore file 34 | const ig = ignore().add(dockerignore) 35 | const lsService = new Ls(this.directory, undefined, this.excludeEnvFile) 36 | const dotenvFiles = lsService.run() 37 | dotenvFiles.forEach(_file => { 38 | count += 1 39 | 40 | const file = path.join(this.directory, _file) // to handle when directory argument passed 41 | 42 | // check if that file is being ignored 43 | if (ig.ignores(file)) { 44 | if (file === '.env.example' || file === '.env.vault') { 45 | const warning = new Error(`[dotenvx@${packageJson.version}][prebuild] ${file} (currently ignored but should not be)`) 46 | warning.help = `[dotenvx@${packageJson.version}][prebuild] ⮕ run [dotenvx ext gitignore --pattern !${file}]` 47 | warnings.push(warning) 48 | } 49 | } else { 50 | if (file !== '.env.example' && file !== '.env.vault') { 51 | const src = fsx.readFileX(file) 52 | const encrypted = isFullyEncrypted(src) 53 | 54 | // if contents are encrypted don't raise an error 55 | if (!encrypted) { 56 | let errorMsg = `[dotenvx@${packageJson.version}][prebuild] ${file} not protected (encrypted or dockerignored)` 57 | let errorHelp = `[dotenvx@${packageJson.version}][prebuild] ⮕ run [dotenvx encrypt -f ${file}] or [dotenvx ext gitignore --pattern ${file}]` 58 | if (file.includes('.env.keys')) { 59 | errorMsg = `[dotenvx@${packageJson.version}][prebuild] ${file} not protected (dockerignored)` 60 | errorHelp = `[dotenvx@${packageJson.version}][prebuild] ⮕ run [dotenvx ext gitignore --pattern ${file}]` 61 | } 62 | 63 | const error = new Error(errorMsg) 64 | error.help = errorHelp 65 | throw error 66 | } 67 | } 68 | } 69 | }) 70 | 71 | let successMessage = `[dotenvx@${packageJson.version}][prebuild] .env files (${count}) protected (encrypted or dockerignored)` 72 | 73 | if (count === 0) { 74 | successMessage = `[dotenvx@${packageJson.version}][prebuild] zero .env files` 75 | } 76 | if (warnings.length > 0) { 77 | successMessage += ` with warnings (${warnings.length})` 78 | } 79 | 80 | return { 81 | successMessage, 82 | warnings 83 | } 84 | } 85 | } 86 | 87 | module.exports = Prebuild 88 | -------------------------------------------------------------------------------- /src/shared/colors.js: -------------------------------------------------------------------------------- 1 | const depth = require('../lib/helpers/colorDepth') 2 | 3 | const colors16 = new Map([ 4 | ['blue', 34], 5 | ['gray', 37], 6 | ['green', 32], 7 | ['olive', 33], 8 | ['orangered', 31], // mapped to red 9 | ['plum', 35], // mapped to magenta 10 | ['red', 31], 11 | ['electricblue', 36], 12 | ['dodgerblue', 36] 13 | ]) 14 | 15 | const colors256 = new Map([ 16 | ['blue', 21], 17 | ['gray', 244], 18 | ['green', 34], 19 | ['olive', 142], 20 | ['orangered', 202], 21 | ['plum', 182], 22 | ['red', 196], 23 | ['electricblue', 45], 24 | ['dodgerblue', 33] 25 | ]) 26 | 27 | function getColor (color) { 28 | const colorDepth = depth.getColorDepth() 29 | if (!colors256.has(color)) { 30 | throw new Error(`Invalid color ${color}`) 31 | } 32 | if (colorDepth >= 8) { 33 | const code = colors256.get(color) 34 | return (message) => `\x1b[38;5;${code}m${message}\x1b[39m` 35 | } 36 | if (colorDepth >= 4) { 37 | const code = colors16.get(color) 38 | return (message) => `\x1b[${code}m${message}\x1b[39m` 39 | } 40 | return (message) => message 41 | } 42 | 43 | function bold (message) { 44 | if (depth.getColorDepth() >= 4) { 45 | return `\x1b[1m${message}\x1b[22m` 46 | } 47 | 48 | return message 49 | } 50 | 51 | module.exports = { 52 | getColor, 53 | bold 54 | } 55 | -------------------------------------------------------------------------------- /src/shared/logger.js: -------------------------------------------------------------------------------- 1 | const packageJson = require('../lib/helpers/packageJson') 2 | const { getColor, bold } = require('./colors') 3 | 4 | const levels = { 5 | error: 0, 6 | warn: 1, 7 | success: 2, 8 | successv: 2, 9 | info: 2, 10 | help: 2, 11 | verbose: 4, 12 | debug: 5, 13 | silly: 6 14 | } 15 | 16 | const error = (m) => bold(getColor('red')(m)) 17 | const warn = getColor('orangered') 18 | const success = getColor('green') 19 | const successv = getColor('olive') // yellow-ish tint that 'looks' like dotenv 20 | const help = getColor('dodgerblue') 21 | const verbose = getColor('plum') 22 | const debug = getColor('plum') 23 | 24 | let currentLevel = levels.info // default log level 25 | let currentName = 'dotenvx' // default logger name 26 | let currentVersion = packageJson.version // default logger version 27 | 28 | function stderr (level, message) { 29 | const formattedMessage = formatMessage(level, message) 30 | console.error(formattedMessage) 31 | } 32 | 33 | function stdout (level, message) { 34 | if (levels[level] === undefined) { 35 | throw new Error(`MISSING_LOG_LEVEL: '${level}'. implement in logger.`) 36 | } 37 | 38 | if (levels[level] <= currentLevel) { 39 | const formattedMessage = formatMessage(level, message) 40 | console.log(formattedMessage) 41 | } 42 | } 43 | 44 | function formatMessage (level, message) { 45 | const formattedMessage = typeof message === 'object' ? JSON.stringify(message) : message 46 | 47 | switch (level.toLowerCase()) { 48 | // errors 49 | case 'error': 50 | return error(formattedMessage) 51 | // warns 52 | case 'warn': 53 | return warn(formattedMessage) 54 | // successes 55 | case 'success': 56 | return success(formattedMessage) 57 | case 'successv': // success with 'version' 58 | return successv(`[${currentName}@${currentVersion}] ${formattedMessage}`) 59 | // info 60 | case 'info': 61 | return formattedMessage 62 | // help 63 | case 'help': 64 | return help(formattedMessage) 65 | // verbose 66 | case 'verbose': 67 | return verbose(formattedMessage) 68 | // debug 69 | case 'debug': 70 | return debug(formattedMessage) 71 | } 72 | } 73 | 74 | const logger = { 75 | // track level 76 | level: 'info', 77 | 78 | // errors 79 | error: (msg) => stderr('error', msg), 80 | // warns 81 | warn: (msg) => stdout('warn', msg), 82 | // success 83 | success: (msg) => stdout('success', msg), 84 | successv: (msg) => stdout('successv', msg), 85 | // info 86 | info: (msg) => stdout('info', msg), 87 | // help 88 | help: (msg) => stdout('help', msg), 89 | // verbose 90 | verbose: (msg) => stdout('verbose', msg), 91 | // debug 92 | debug: (msg) => stdout('debug', msg), 93 | setLevel: (level) => { 94 | if (levels[level] !== undefined) { 95 | currentLevel = levels[level] 96 | logger.level = level 97 | } 98 | }, 99 | setName: (name) => { 100 | currentName = name 101 | logger.name = name 102 | }, 103 | setVersion: (version) => { 104 | currentVersion = version 105 | logger.version = version 106 | } 107 | } 108 | 109 | function setLogLevel (options) { 110 | const logLevel = options.debug 111 | ? 'debug' 112 | : options.verbose 113 | ? 'verbose' 114 | : options.quiet 115 | ? 'error' 116 | : options.logLevel 117 | 118 | if (!logLevel) return 119 | logger.setLevel(logLevel) 120 | // Only log which level it's setting if it's not set to quiet mode 121 | if (!options.quiet || (options.quiet && logLevel !== 'error')) { 122 | logger.debug(`Setting log level to ${logLevel}`) 123 | } 124 | } 125 | 126 | function setLogName (options) { 127 | const logName = options.logName 128 | if (!logName) return 129 | logger.setName(logName) 130 | } 131 | 132 | function setLogVersion (options) { 133 | const logVersion = options.logVersion 134 | if (!logVersion) return 135 | logger.setVersion(logVersion) 136 | } 137 | 138 | module.exports = { 139 | logger, 140 | getColor, 141 | setLogLevel, 142 | setLogName, 143 | setLogVersion, 144 | levels 145 | } 146 | -------------------------------------------------------------------------------- /tests/.env: -------------------------------------------------------------------------------- 1 | BASIC=basic 2 | 3 | # previous line intentionally left blank 4 | AFTER_LINE=after_line 5 | EMPTY= 6 | EMPTY_SINGLE_QUOTES='' 7 | EMPTY_DOUBLE_QUOTES="" 8 | EMPTY_BACKTICKS=`` 9 | SINGLE_QUOTES='single_quotes' 10 | SINGLE_QUOTES_SPACED=' single quotes ' 11 | DOUBLE_QUOTES="double_quotes" 12 | DOUBLE_QUOTES_SPACED=" double quotes " 13 | DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes' 14 | DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET="{ port: $MONGOLAB_PORT}" 15 | SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes" 16 | BACKTICKS_INSIDE_SINGLE='`backticks` work inside single quotes' 17 | BACKTICKS_INSIDE_DOUBLE="`backticks` work inside double quotes" 18 | BACKTICKS=`backticks` 19 | BACKTICKS_SPACED=` backticks ` 20 | DOUBLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" work inside backticks` 21 | SINGLE_QUOTES_INSIDE_BACKTICKS=`single 'quotes' work inside backticks` 22 | DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" and single 'quotes' work inside backticks` 23 | EXPAND_NEWLINES="expand\nnew\nlines" 24 | DONT_EXPAND_UNQUOTED=dontexpand\nnewlines 25 | DONT_EXPAND_SQUOTED='dontexpand\nnewlines' 26 | # COMMENTS=work 27 | INLINE_COMMENTS=inline comments # work #very #well 28 | INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work 29 | INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work 30 | INLINE_COMMENTS_BACKTICKS=`inline comments outside of #backticks` # work 31 | INLINE_COMMENTS_SPACE=inline comments start with a#number sign. no space required. 32 | EQUAL_SIGNS=equals== 33 | RETAIN_INNER_QUOTES={"foo": "bar"} 34 | RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' 35 | RETAIN_INNER_QUOTES_AS_BACKTICKS=`{"foo": "bar's"}` 36 | TRIM_SPACE_FROM_UNQUOTED= some spaced out string 37 | USERNAME=therealnerdybeast@example.tld 38 | SPACED_KEY = parsed 39 | -------------------------------------------------------------------------------- /tests/.env.eval: -------------------------------------------------------------------------------- 1 | HELLO="$(echo world)" 2 | -------------------------------------------------------------------------------- /tests/.env.export: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | export KEY=value 3 | -------------------------------------------------------------------------------- /tests/.env.latin1: -------------------------------------------------------------------------------- 1 | HELLO="latin1" -------------------------------------------------------------------------------- /tests/.env.local: -------------------------------------------------------------------------------- 1 | BASIC=local_basic 2 | LOCAL=local 3 | -------------------------------------------------------------------------------- /tests/.env.multiline: -------------------------------------------------------------------------------- 1 | BASIC=basic 2 | 3 | # previous line intentionally left blank 4 | AFTER_LINE=after_line 5 | EMPTY= 6 | SINGLE_QUOTES='single_quotes' 7 | SINGLE_QUOTES_SPACED=' single quotes ' 8 | DOUBLE_QUOTES="double_quotes" 9 | DOUBLE_QUOTES_SPACED=" double quotes " 10 | EXPAND_NEWLINES="expand\nnew\nlines" 11 | DONT_EXPAND_UNQUOTED=dontexpand\nnewlines 12 | DONT_EXPAND_SQUOTED='dontexpand\nnewlines' 13 | # COMMENTS=work 14 | EQUAL_SIGNS=equals== 15 | RETAIN_INNER_QUOTES={"foo": "bar"} 16 | 17 | RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' 18 | TRIM_SPACE_FROM_UNQUOTED= some spaced out string 19 | USERNAME=therealnerdybeast@example.tld 20 | SPACED_KEY = parsed 21 | 22 | MULTI_DOUBLE_QUOTED="THIS 23 | IS 24 | A 25 | MULTILINE 26 | STRING" 27 | 28 | MULTI_SINGLE_QUOTED='THIS 29 | IS 30 | A 31 | MULTILINE 32 | STRING' 33 | 34 | MULTI_BACKTICKED=`THIS 35 | IS 36 | A 37 | "MULTILINE'S" 38 | STRING` 39 | 40 | MULTI_PEM_DOUBLE_QUOTED="-----BEGIN PUBLIC KEY----- 41 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u 42 | LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/ 43 | bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/ 44 | kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V 45 | u4QuUoobAgMBAAE= 46 | -----END PUBLIC KEY-----" 47 | -------------------------------------------------------------------------------- /tests/.env.utf16le: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotenvx/dotenvx/49a78cacab5fa4b758b901f1ac739cccf2bf4562/tests/.env.utf16le -------------------------------------------------------------------------------- /tests/.env.vault: -------------------------------------------------------------------------------- 1 | DOTENV_VAULT_DEVELOPMENT="s7NYXa809k/bVSPwIAmJhPJmEGTtU0hG58hOZy7I0ix6y5HP8LsHBsZCYC/gw5DDFy5DgOcyd18R" 2 | -------------------------------------------------------------------------------- /tests/cli/actions/ext/genexample.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const fsx = require('../../../../src/lib/helpers/fsx') 3 | const sinon = require('sinon') 4 | 5 | const main = require('../../../../src/lib/main') 6 | const genexample = require('../../../../src/cli/actions/ext/genexample') 7 | 8 | t.test('genexample calls main.genexample', ct => { 9 | const stub = sinon.stub(main, 'genexample') 10 | stub.returns({ 11 | envExampleFile: 'HELLO=""', 12 | envFile: '.env.example', 13 | exampleFilepath: '.env.example', 14 | addedKeys: ['HELLO'] 15 | }) 16 | 17 | const fsStub = sinon.stub(fsx, 'writeFileX') 18 | 19 | const optsStub = sinon.stub().returns({}) 20 | const fakeContext = { 21 | opts: optsStub 22 | } 23 | 24 | // Call the genexample function with the fake context 25 | genexample.call(fakeContext, '.') 26 | 27 | t.ok(stub.called, 'main.genexample() called') 28 | t.ok(fsStub.called, 'fs.writeFileX() called') 29 | stub.restore() 30 | fsStub.restore() 31 | 32 | ct.end() 33 | }) 34 | 35 | t.test('genexample calls main.genexample (no addedKeys changes)', ct => { 36 | const stub = sinon.stub(main, 'genexample') 37 | stub.returns({ 38 | envExampleFile: '', 39 | envFile: '.env.example', 40 | exampleFilepath: '.env.example', 41 | addedKeys: [] 42 | }) 43 | 44 | const fsStub = sinon.stub(fsx, 'writeFileX') 45 | 46 | const optsStub = sinon.stub().returns({}) 47 | const fakeContext = { 48 | opts: optsStub 49 | } 50 | 51 | // Call the genexample function with the fake context 52 | genexample.call(fakeContext, '.') 53 | 54 | t.ok(stub.called, 'main.genexample() called') 55 | t.ok(fsStub.called, 'fsx.writeFileX() called') 56 | stub.restore() 57 | fsStub.restore() 58 | 59 | ct.end() 60 | }) 61 | 62 | t.test('genexample calls main.genexample (other error)', ct => { 63 | const stub = sinon.stub(main, 'genexample').throws(new Error('other error')) 64 | const exitStub = sinon.stub(process, 'exit') 65 | 66 | const optsStub = sinon.stub().returns({}) 67 | const fakeContext = { 68 | opts: optsStub 69 | } 70 | 71 | // Call the genexample function with the fake context 72 | genexample.call(fakeContext, '.') 73 | 74 | ct.ok(exitStub.calledWith(1), 'process.exit was called with code 1') 75 | 76 | stub.restore() 77 | exitStub.restore() 78 | 79 | ct.end() 80 | }) 81 | 82 | t.test('genexample calls main.genexample (error with code and help message)', ct => { 83 | const error = new Error('message') 84 | error.help = 'help message' 85 | error.code = 'CODE' 86 | 87 | const stub = sinon.stub(main, 'genexample').throws(error) 88 | const exitStub = sinon.stub(process, 'exit') 89 | 90 | const optsStub = sinon.stub().returns({}) 91 | const fakeContext = { 92 | opts: optsStub 93 | } 94 | 95 | // Call the genexample function with the fake context 96 | genexample.call(fakeContext, '.') 97 | 98 | ct.ok(exitStub.calledWith(1), 'process.exit was called with code 1') 99 | 100 | stub.restore() 101 | exitStub.restore() 102 | 103 | ct.end() 104 | }) 105 | -------------------------------------------------------------------------------- /tests/cli/actions/ext/prebuild.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const sinon = require('sinon') 3 | 4 | const prebuild = require('../../../../src/cli/actions/ext/prebuild') 5 | 6 | const { logger } = require('../../../../src/shared/logger') 7 | const Prebuild = require('../../../../src/lib/services/prebuild') 8 | 9 | const optsStub = sinon.stub().returns({}) 10 | const fakeContext = { opts: optsStub } 11 | 12 | t.beforeEach((ct) => { 13 | sinon.restore() 14 | }) 15 | 16 | t.test('prebuild - successMessage', (ct) => { 17 | // Stub the Prebuild service 18 | sinon.stub(Prebuild.prototype, 'run').returns({ 19 | successMessage: 'success', 20 | warnings: [] 21 | }) 22 | 23 | const loggerSuccessStub = sinon.stub(logger, 'success') 24 | 25 | prebuild.call(fakeContext) 26 | 27 | ct.ok(loggerSuccessStub.calledWith('success'), 'logger.success logs') 28 | 29 | ct.end() 30 | }) 31 | 32 | t.test('prebuild - success with warnings', (ct) => { 33 | const warning = new Error('.dockerignore missing') 34 | warning.help = '? add it with [touch .dockerignore]' 35 | // Stub the Prebuild service 36 | sinon.stub(Prebuild.prototype, 'run').returns({ 37 | successMessage: 'success (with 1 warning)', 38 | warnings: [warning] 39 | }) 40 | 41 | const loggerSuccessStub = sinon.stub(logger, 'success') 42 | const loggerWarnStub = sinon.stub(logger, 'warn') 43 | const loggerHelpStub = sinon.stub(logger, 'help') 44 | 45 | prebuild.call(fakeContext) 46 | 47 | ct.ok(loggerSuccessStub.calledWith('success (with 1 warning)'), 'logger.success logs') 48 | ct.ok(loggerWarnStub.calledWith('.dockerignore missing'), 'logger.warn logs') 49 | ct.ok(loggerHelpStub.calledWith('? add it with [touch .dockerignore]'), 'logger.help logs') 50 | 51 | ct.end() 52 | }) 53 | 54 | t.test('prebuild - error raised', (ct) => { 55 | sinon.stub(Prebuild.prototype, 'run').throws({ 56 | message: 'An error occurred', 57 | help: 'Help message for error' 58 | }) 59 | 60 | const processExitStub = sinon.stub(process, 'exit') 61 | const loggerErrorStub = sinon.stub(logger, 'error') 62 | const loggerHelpStub = sinon.stub(logger, 'help') 63 | 64 | prebuild.call(fakeContext) 65 | 66 | ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1') 67 | ct.ok(loggerErrorStub.calledWith('An error occurred'), 'logger.success logs') 68 | ct.ok(loggerHelpStub.calledWith('Help message for error'), 'logger.help logs') 69 | 70 | ct.end() 71 | }) 72 | -------------------------------------------------------------------------------- /tests/cli/actions/ext/precommit.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const sinon = require('sinon') 3 | 4 | const precommit = require('../../../../src/cli/actions/ext/precommit') 5 | 6 | const { logger } = require('../../../../src/shared/logger') 7 | const Precommit = require('../../../../src/lib/services/precommit') 8 | 9 | const optsStub = sinon.stub().returns({}) 10 | const fakeContext = { opts: optsStub } 11 | 12 | t.beforeEach((ct) => { 13 | sinon.restore() 14 | }) 15 | 16 | t.test('precommit - successMessage', (ct) => { 17 | // Stub the Precommit service 18 | sinon.stub(Precommit.prototype, 'run').returns({ 19 | successMessage: 'success', 20 | warnings: [] 21 | }) 22 | 23 | const loggerSuccessStub = sinon.stub(logger, 'success') 24 | 25 | precommit.call(fakeContext) 26 | 27 | ct.ok(loggerSuccessStub.calledWith('success'), 'logger.success logs') 28 | 29 | ct.end() 30 | }) 31 | 32 | t.test('precommit - success with warnings', (ct) => { 33 | const warning = new Error('.gitignore missing') 34 | warning.help = '? add it with [touch .gitignore]' 35 | // Stub the Precommit service 36 | sinon.stub(Precommit.prototype, 'run').returns({ 37 | successMessage: 'success (with 1 warning)', 38 | warnings: [warning] 39 | }) 40 | 41 | const loggerSuccessStub = sinon.stub(logger, 'success') 42 | const loggerWarnStub = sinon.stub(logger, 'warn') 43 | const loggerHelpStub = sinon.stub(logger, 'help') 44 | 45 | precommit.call(fakeContext) 46 | 47 | ct.ok(loggerSuccessStub.calledWith('success (with 1 warning)'), 'logger.success logs') 48 | ct.ok(loggerWarnStub.calledWith('.gitignore missing'), 'logger.warn logs') 49 | ct.ok(loggerHelpStub.calledWith('? add it with [touch .gitignore]'), 'logger.help logs') 50 | 51 | ct.end() 52 | }) 53 | 54 | t.test('precommit - error raised', (ct) => { 55 | sinon.stub(Precommit.prototype, 'run').throws({ 56 | message: 'An error occurred', 57 | help: 'Help message for error' 58 | }) 59 | 60 | const processExitStub = sinon.stub(process, 'exit') 61 | const loggerErrorStub = sinon.stub(logger, 'error') 62 | const loggerHelpStub = sinon.stub(logger, 'help') 63 | 64 | precommit.call(fakeContext) 65 | 66 | ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1') 67 | ct.ok(loggerErrorStub.calledWith('An error occurred'), 'logger.success logs') 68 | ct.ok(loggerHelpStub.calledWith('Help message for error'), 'logger.help logs') 69 | 70 | ct.end() 71 | }) 72 | -------------------------------------------------------------------------------- /tests/cli/actions/ext/scan.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const sinon = require('sinon') 3 | const childProcess = require('child_process') 4 | 5 | const scan = require('../../../../src/cli/actions/ext/scan') 6 | 7 | const { logger } = require('../../../../src/shared/logger') 8 | 9 | const optsStub = sinon.stub().returns({}) 10 | const fakeContext = { opts: optsStub } 11 | const originalExecSync = childProcess.execSync 12 | 13 | t.beforeEach((ct) => { 14 | sinon.restore() 15 | childProcess.execSync = sinon.stub() 16 | }) 17 | 18 | t.afterEach((ct) => { 19 | childProcess.execSync = originalExecSync // restore the original execSync after each test 20 | }) 21 | 22 | t.test('scan - gitleaks not installed', (ct) => { 23 | childProcess.execSync.throws(new Error('gitleaks: command not found')) 24 | 25 | const processExitStub = sinon.stub(process, 'exit') 26 | const loggerErrorStub = sinon.stub(logger, 'error') 27 | const loggerHelpStub = sinon.stub(logger, 'help') 28 | 29 | scan.call(fakeContext) 30 | 31 | ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1') 32 | ct.ok(loggerErrorStub.calledWith('gitleaks: command not found'), 'logger.error logs') 33 | ct.ok(loggerHelpStub.calledWith('? install gitleaks: [brew install gitleaks]'), 'logger.help logs') 34 | ct.ok(loggerHelpStub.calledWith('? other install options: [https://github.com/gitleaks/gitleaks]'), 'logger.help logs') 35 | 36 | ct.end() 37 | }) 38 | 39 | t.test('scan - gitleaks installed and works', (ct) => { 40 | const gitleaksOutput = ` 41 | ○ 42 | │╲ 43 | │ ○ 44 | ○ ░ 45 | ░ gitleaks 46 | 47 | 10:22AM INF 0 commits scanned. 48 | 10:22AM INF scan completed in 14.4ms 49 | 10:22AM INF no leaks found 50 | ` 51 | 52 | childProcess.execSync.onCall(0).returns('8.18.4') 53 | childProcess.execSync.onCall(1).returns(gitleaksOutput) 54 | 55 | const loggerInfoStub = sinon.stub(logger, 'info') 56 | 57 | scan.call(fakeContext) 58 | 59 | ct.ok(loggerInfoStub.calledWith(gitleaksOutput), 'logger.info logs') 60 | 61 | ct.end() 62 | }) 63 | 64 | t.test('scan - gitleaks installed and raises error', (ct) => { 65 | childProcess.execSync.onCall(0).returns('8.18.4') 66 | 67 | // Simulate gitleaks error with stderr output 68 | const error = new Error('Gitleaks failed') 69 | error.stdout = Buffer.from('leak: API_KEY=abcd1234') 70 | childProcess.execSync.onCall(1).throws(error) 71 | 72 | const processExitStub = sinon.stub(process, 'exit') 73 | const loggerErrorStub = sinon.stub(logger, 'error') 74 | 75 | scan.call(fakeContext) 76 | 77 | ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1') 78 | ct.ok(loggerErrorStub.calledWith('leak: API_KEY=abcd1234'), 'logger.error logs') 79 | 80 | ct.end() 81 | }) 82 | -------------------------------------------------------------------------------- /tests/cli/actions/keypair.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const sinon = require('sinon') 3 | const capcon = require('capture-console') 4 | 5 | const main = require('./../../../src/lib/main') 6 | 7 | const keypair = require('./../../../src/cli/actions/keypair') 8 | 9 | t.beforeEach((ct) => { 10 | sinon.restore() 11 | }) 12 | 13 | t.test('keypair', ct => { 14 | const optsStub = sinon.stub().returns({}) 15 | const fakeContext = { opts: optsStub } 16 | const stub = sinon.stub(main, 'keypair').returns({ DOTENV_PUBLIC_KEY: '<publicKey>', DOTENV_PRIVATE_KEY: '<privateKey>' }) 17 | 18 | const stdout = capcon.interceptStdout(() => { 19 | keypair.call(fakeContext, undefined) 20 | }) 21 | 22 | t.ok(stub.called, 'main.keypair() called') 23 | t.equal(stdout, `${JSON.stringify({ DOTENV_PUBLIC_KEY: '<publicKey>', DOTENV_PRIVATE_KEY: '<privateKey>' }, null, 0)}\n`) 24 | 25 | ct.end() 26 | }) 27 | 28 | t.test('keypair KEY', ct => { 29 | const optsStub = sinon.stub().returns({}) 30 | const fakeContext = { opts: optsStub } 31 | const stub = sinon.stub(main, 'keypair').returns('<publicKey>') 32 | 33 | const stdout = capcon.interceptStdout(() => { 34 | keypair.call(fakeContext, 'DOTENV_PUBLIC_KEY') 35 | }) 36 | 37 | t.ok(stub.called, 'main.keypair() called') 38 | t.equal(stdout, '<publicKey>\n') 39 | 40 | ct.end() 41 | }) 42 | 43 | t.test('keypair --format shell', ct => { 44 | const optsStub = sinon.stub().returns({ format: 'shell' }) 45 | const fakeContext = { opts: optsStub } 46 | const stub = sinon.stub(main, 'keypair').returns({ DOTENV_PUBLIC_KEY: '<publicKey>', DOTENV_PRIVATE_KEY: '<privateKey>' }) 47 | 48 | const stdout = capcon.interceptStdout(() => { 49 | keypair.call(fakeContext, undefined) 50 | }) 51 | 52 | t.ok(stub.called, 'main.keypair() called') 53 | t.equal(stdout, 'DOTENV_PUBLIC_KEY=<publicKey> DOTENV_PRIVATE_KEY=<privateKey>\n') 54 | 55 | ct.end() 56 | }) 57 | 58 | t.test('keypair --format shell (when null value should be empty string for shell format)', ct => { 59 | const optsStub = sinon.stub().returns({ format: 'shell' }) 60 | const fakeContext = { opts: optsStub } 61 | const stub = sinon.stub(main, 'keypair').returns({ DOTENV_PUBLIC_KEY: '<publicKey>', DOTENV_PRIVATE_KEY: null }) 62 | 63 | const stdout = capcon.interceptStdout(() => { 64 | keypair.call(fakeContext, undefined) 65 | }) 66 | 67 | t.ok(stub.called, 'main.keypair() called') 68 | t.equal(stdout, 'DOTENV_PUBLIC_KEY=<publicKey> DOTENV_PRIVATE_KEY=\n') 69 | 70 | ct.end() 71 | }) 72 | 73 | t.test('keypair --pretty-print', ct => { 74 | const optsStub = sinon.stub().returns({ prettyPrint: true }) 75 | const fakeContext = { opts: optsStub } 76 | const stub = sinon.stub(main, 'keypair').returns({ DOTENV_PUBLIC_KEY: '<publicKey>' }) 77 | 78 | const stdout = capcon.interceptStdout(() => { 79 | keypair.call(fakeContext, undefined) 80 | }) 81 | 82 | t.ok(stub.called, 'main.keypair() called') 83 | t.equal(stdout, `${JSON.stringify({ DOTENV_PUBLIC_KEY: '<publicKey>' }, null, 2)}\n`) 84 | 85 | ct.end() 86 | }) 87 | 88 | t.test('keypair KEY (not found)', ct => { 89 | const optsStub = sinon.stub().returns({}) 90 | const fakeContext = { opts: optsStub } 91 | const stub = sinon.stub(main, 'keypair').returns(undefined) 92 | const processExitStub = sinon.stub(process, 'exit') 93 | 94 | const stdout = capcon.interceptStdout(() => { 95 | keypair.call(fakeContext, 'NOTFOUND') 96 | }) 97 | 98 | t.ok(stub.called, 'main.keypair() called') 99 | t.ok(processExitStub.calledWith(1), 'process.exit(1)') 100 | t.equal(stdout, '\n') // send empty string if key's value undefined 101 | 102 | ct.end() 103 | }) 104 | -------------------------------------------------------------------------------- /tests/cli/actions/ls.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const sinon = require('sinon') 3 | 4 | const main = require('../../../src/lib/main') 5 | const ls = require('../../../src/cli/actions/ls') 6 | 7 | t.test('ls calls main.ls', ct => { 8 | const stub = sinon.stub(main, 'ls') 9 | stub.returns({}) 10 | 11 | const optsStub = sinon.stub().returns({}) 12 | const fakeContext = { 13 | opts: optsStub 14 | } 15 | 16 | // Call the ls function with the fake context 17 | ls.call(fakeContext, '.') 18 | 19 | t.ok(stub.called, 'main.ls() called') 20 | stub.restore() 21 | 22 | ct.end() 23 | }) 24 | -------------------------------------------------------------------------------- /tests/cli/examples.test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | const { 3 | run, 4 | precommit, 5 | prebuild, 6 | gitignore, 7 | set 8 | } = require('../../src/cli/examples') // Adjust the path as needed 9 | 10 | tap.test('run function returns expected string', (t) => { 11 | const expected = ` 12 | Examples: 13 | 14 | \`\`\` 15 | $ dotenvx run -- npm run dev 16 | $ dotenvx run -- flask --app index run 17 | $ dotenvx run -- php artisan serve 18 | $ dotenvx run -- bin/rails s 19 | \`\`\` 20 | 21 | Try it: 22 | 23 | \`\`\` 24 | $ echo "HELLO=World" > .env 25 | $ echo "console.log('Hello ' + process.env.HELLO)" > index.js 26 | 27 | $ dotenvx run -f .env -- node index.js 28 | [dotenvx] injecting env (1) from .env 29 | Hello World 30 | \`\`\` 31 | ` 32 | t.equal(run(), expected) 33 | t.end() 34 | }) 35 | 36 | tap.test('precommit function returns expected string', (t) => { 37 | const expected = ` 38 | Examples: 39 | 40 | \`\`\` 41 | $ dotenvx ext precommit 42 | $ dotenvx ext precommit --install 43 | \`\`\` 44 | 45 | Try it: 46 | 47 | \`\`\` 48 | $ dotenvx ext precommit 49 | [dotenvx@0.45.0][precommit] success 50 | \`\`\` 51 | ` 52 | t.equal(precommit(), expected) 53 | t.end() 54 | }) 55 | 56 | tap.test('prebuild function returns expected string', (t) => { 57 | const expected = ` 58 | Examples: 59 | 60 | \`\`\` 61 | $ dotenvx ext prebuild 62 | \`\`\` 63 | 64 | Try it: 65 | 66 | \`\`\` 67 | $ dotenvx ext prebuild 68 | [dotenvx@0.10.0][prebuild] success 69 | \`\`\` 70 | ` 71 | t.equal(prebuild(), expected) 72 | t.end() 73 | }) 74 | 75 | tap.test('gitignore function returns expected string', (t) => { 76 | const expected = ` 77 | Examples: 78 | 79 | \`\`\` 80 | $ dotenvx ext gitignore 81 | $ dotenvx ext gitignore --pattern .env.keys 82 | \`\`\` 83 | 84 | Try it: 85 | 86 | \`\`\` 87 | $ dotenvx ext gitignore 88 | ✔ ignored .env* (.gitignore) 89 | \`\`\` 90 | ` 91 | t.equal(gitignore(), expected) 92 | t.end() 93 | }) 94 | 95 | tap.test('set function returns expected string', (t) => { 96 | const expected = ` 97 | Examples: 98 | 99 | \`\`\` 100 | $ dotenvx set KEY value 101 | $ dotenvx set KEY "value with spaces" 102 | $ dotenvx set KEY -- "---value with a dash---" 103 | $ dotenvx set KEY -- "-----BEGIN OPENSSH PRIVATE KEY----- 104 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 105 | -----END OPENSSH PRIVATE KEY-----" 106 | \`\`\` 107 | ` 108 | t.equal(set(), expected) 109 | t.end() 110 | }) 111 | -------------------------------------------------------------------------------- /tests/e2e/encrypt.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const fs = require('fs') 3 | const os = require('os') 4 | const path = require('path') 5 | const which = require('which') 6 | const dotenv = require('dotenv') 7 | const { execSync } = require('child_process') 8 | 9 | const keypair = require('../../src/lib/helpers/keypair') 10 | 11 | let tempDir = '' 12 | const osTempDir = fs.realpathSync(os.tmpdir()) 13 | const originalDir = process.cwd() 14 | 15 | const node = path.resolve(which.sync('node')) // /opt/homebrew/node 16 | const dotenvx = `${node} ${path.join(originalDir, 'src/cli/dotenvx.js')}` 17 | 18 | function execShell (commands) { 19 | return execSync(commands, { 20 | encoding: 'utf8', 21 | shell: true 22 | }).trim() 23 | } 24 | 25 | t.beforeEach((ct) => { 26 | // important, clear process.env before each test 27 | process.env = {} 28 | 29 | tempDir = fs.mkdtempSync(path.join(osTempDir, 'dotenvx-test-')) 30 | 31 | // go to tempDir 32 | process.chdir(tempDir) 33 | }) 34 | 35 | t.afterEach((ct) => { 36 | // cleanup 37 | process.chdir(originalDir) 38 | }) 39 | 40 | t.test('#encrypt', ct => { 41 | execShell(` 42 | echo "HELLO=World" > .env 43 | `) 44 | 45 | const output = execShell(`${dotenvx} encrypt`) 46 | 47 | const parsedEnvKeys = dotenv.parse(fs.readFileSync(path.join(tempDir, '.env.keys'))) 48 | const DOTENV_PRIVATE_KEY = parsedEnvKeys.DOTENV_PRIVATE_KEY 49 | 50 | ct.equal(output, `✔ encrypted (.env) 51 | ✔ key added to .env.keys (DOTENV_PRIVATE_KEY) 52 | ⮕ next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys 53 | ⮕ next run [DOTENV_PRIVATE_KEY='${DOTENV_PRIVATE_KEY}' dotenvx run -- yourcommand] to test decryption locally`) 54 | 55 | ct.end() 56 | }) 57 | 58 | t.test('#encrypt -k', ct => { 59 | ct.plan(4) 60 | 61 | execShell(` 62 | echo "HELLO=World\nHI=thar" > .env 63 | `) 64 | 65 | const output = execShell(`${dotenvx} encrypt -k HI`) 66 | 67 | const parsedEnvKeys = dotenv.parse(fs.readFileSync(path.join(tempDir, '.env.keys'))) 68 | const DOTENV_PRIVATE_KEY = parsedEnvKeys.DOTENV_PRIVATE_KEY 69 | 70 | ct.equal(output, `✔ encrypted (.env) 71 | ✔ key added to .env.keys (DOTENV_PRIVATE_KEY) 72 | ⮕ next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys 73 | ⮕ next run [DOTENV_PRIVATE_KEY='${DOTENV_PRIVATE_KEY}' dotenvx run -- yourcommand] to test decryption locally`) 74 | 75 | execShell('rm .env.keys') 76 | 77 | ct.equal(execShell(`${dotenvx} get HELLO`), 'World') // unencrypted still 78 | ct.match(execShell(`${dotenvx} get HI`), /^encrypted:/, 'HI should be encrypted') 79 | 80 | process.env.DOTENV_PRIVATE_KEY = DOTENV_PRIVATE_KEY 81 | ct.equal(execShell(`${dotenvx} get HI`), 'thar') 82 | 83 | ct.end() 84 | }) 85 | 86 | t.test('#run - encrypt -k --stdout', ct => { 87 | execShell(` 88 | echo "HELLO=World\nHI=thar" > .env 89 | `) 90 | 91 | const output = execShell(`${dotenvx} encrypt -k HI --stdout`) 92 | 93 | const parsedEnvKeys = dotenv.parse(fs.readFileSync(path.join(tempDir, '.env.keys'))) 94 | const DOTENV_PRIVATE_KEY = parsedEnvKeys.DOTENV_PRIVATE_KEY 95 | const { publicKey } = keypair(DOTENV_PRIVATE_KEY) 96 | 97 | const expectedFixedPart1 = `#/-------------------[DOTENV_PUBLIC_KEY]--------------------/ 98 | #/ public-key encryption for .env files / 99 | #/ [how it works](https://dotenvx.com/encryption) / 100 | #/----------------------------------------------------------/ 101 | DOTENV_PUBLIC_KEY="${publicKey}" 102 | 103 | # .env 104 | HELLO=World 105 | HI=encrypted:` 106 | 107 | const parts = output.split('HI=encrypted:') 108 | const encryptedPart = parts[1] 109 | const unencryptedPart = `${parts[0]}HI=encrypted:` 110 | 111 | ct.equal(unencryptedPart, expectedFixedPart1, 'The fixed part of the output should match the expected output') 112 | ct.match(encryptedPart, /.*/, 'The encrypted part should match the expected pattern') 113 | 114 | ct.end() 115 | }) 116 | -------------------------------------------------------------------------------- /tests/e2e/ext.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const path = require('path') 3 | const which = require('which') 4 | const { execSync } = require('child_process') 5 | 6 | const originalDir = process.cwd() 7 | 8 | const node = path.resolve(which.sync('node')) // /opt/homebrew/node 9 | const dotenvx = `${node} ${path.join(originalDir, 'src/cli/dotenvx.js')}` 10 | 11 | function execShell (commands) { 12 | return execSync(commands, { 13 | encoding: 'utf8', 14 | shell: true 15 | }).trim() 16 | } 17 | 18 | t.test('ext', ct => { 19 | const output = execShell(`${dotenvx} ext`) 20 | 21 | t.match(output, /genexample/, 'should say genexample') 22 | t.match(output, /gitignore/, 'should say gitignore') 23 | t.match(output, /prebuild/, 'should say prebuild') 24 | t.match(output, /precommit/, 'should say precommit') 25 | 26 | ct.end() 27 | }) 28 | 29 | t.test('ext missing', ct => { 30 | const output = execShell(`${dotenvx} ext missing`) 31 | 32 | t.match(output, "error: unknown command 'missing'", 'should say installation needed') 33 | 34 | ct.end() 35 | }) 36 | 37 | t.test('ext vault', ct => { 38 | const output = execShell(`${dotenvx} ext vault`) 39 | 40 | t.match(output, /\[INSTALLATION_NEEDED\] install dotenvx-ext-vault to use \[dotenvx ext vault\] commands/, 'should say installation needed') 41 | t.match(output, /see installation instructions/, 'should say see installation instructions') 42 | 43 | ct.end() 44 | }) 45 | -------------------------------------------------------------------------------- /tests/e2e/get.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const fs = require('fs') 3 | const os = require('os') 4 | const path = require('path') 5 | const which = require('which') 6 | const { execSync } = require('child_process') 7 | 8 | let tempDir = '' 9 | const osTempDir = fs.realpathSync(os.tmpdir()) 10 | const originalDir = process.cwd() 11 | 12 | const node = path.resolve(which.sync('node')) // /opt/homebrew/node 13 | const dotenvx = `${node} ${path.join(originalDir, 'src/cli/dotenvx.js')}` 14 | 15 | function execShell (commands) { 16 | return execSync(commands, { 17 | encoding: 'utf8', 18 | shell: true 19 | }).trim() 20 | } 21 | 22 | t.beforeEach((ct) => { 23 | // important, clear process.env before each test 24 | process.env = {} 25 | 26 | tempDir = fs.mkdtempSync(path.join(osTempDir, 'dotenvx-test-')) 27 | 28 | // go to tempDir 29 | process.chdir(tempDir) 30 | }) 31 | 32 | t.afterEach((ct) => { 33 | // cleanup 34 | process.chdir(originalDir) 35 | }) 36 | 37 | t.test('#get', ct => { 38 | execShell(` 39 | echo "HELLO=World" > .env 40 | `) 41 | 42 | ct.equal(execShell(`${dotenvx} get HELLO`), 'World') 43 | 44 | ct.end() 45 | }) 46 | 47 | t.test('#get --env', ct => { 48 | execShell(` 49 | echo "HELLO=World" > .env 50 | `) 51 | 52 | ct.equal(execShell(`${dotenvx} get HELLO --env HELLO=String`), 'World') 53 | ct.equal(execShell(`${dotenvx} get HELLO --env HELLO=String -f .env`), 'String') 54 | ct.equal(execShell(`${dotenvx} get HELLO -f .env --env HELLO=String`), 'World') 55 | 56 | ct.end() 57 | }) 58 | 59 | t.test('#get --overload', ct => { 60 | execShell(` 61 | echo "HELLO=World" > .env 62 | echo "HELLO=production" > .env.production 63 | `) 64 | 65 | ct.equal(execShell(`${dotenvx} get HELLO -f .env.production --env HELLO=String -f .env --overload`), 'World') 66 | 67 | ct.end() 68 | }) 69 | 70 | t.test('#get (json)', ct => { 71 | execShell(` 72 | echo "HELLO=World" > .env 73 | `) 74 | 75 | ct.equal(execShell(`${dotenvx} get`), '{"HELLO":"World"}') 76 | 77 | ct.end() 78 | }) 79 | -------------------------------------------------------------------------------- /tests/e2e/version.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const path = require('path') 3 | const which = require('which') 4 | const { execSync } = require('child_process') 5 | 6 | const packageJson = require('../../src/lib/helpers/packageJson') 7 | const version = packageJson.version 8 | 9 | const originalDir = process.cwd() 10 | 11 | const node = path.resolve(which.sync('node')) // /opt/homebrew/node 12 | const dotenvx = `${node} ${path.join(originalDir, 'src/cli/dotenvx.js')}` 13 | 14 | function execShell (commands) { 15 | return execSync(commands, { 16 | encoding: 'utf8', 17 | shell: true 18 | }).trim() 19 | } 20 | 21 | t.test('#--version', ct => { 22 | ct.equal(execShell(`${dotenvx} --version`), version) 23 | 24 | ct.end() 25 | }) 26 | -------------------------------------------------------------------------------- /tests/lib/helpers/arrayToTree.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const ArrayToTree = require('../../../src/lib/helpers/arrayToTree') 4 | 5 | t.test('#run', ct => { 6 | const arr = [ 7 | '.env', 8 | 'sub1/.env', 9 | 'sub1/sub2/.env', 10 | 'sub1/sub2/sub3/.env' 11 | ] 12 | const tree = new ArrayToTree(arr).run() 13 | 14 | ct.same(tree, { 15 | '.env': {}, 16 | sub1: { 17 | sub2: { 18 | sub3: { 19 | '.env': {} 20 | }, 21 | '.env': {} 22 | }, 23 | '.env': {} 24 | } 25 | }) 26 | 27 | ct.end() 28 | }) 29 | -------------------------------------------------------------------------------- /tests/lib/helpers/colorDepth.test.js: -------------------------------------------------------------------------------- 1 | const { WriteStream } = require('tty') 2 | const t = require('tap') 3 | const sinon = require('sinon') 4 | 5 | const depth = require('../../../src/lib/helpers/colorDepth') 6 | 7 | t.beforeEach((ct) => { 8 | // important, clear process.env before each test 9 | process.env = {} 10 | }) 11 | 12 | t.test('returns WriteStream.prototype.getColorDepth()', (ct) => { 13 | const stub = sinon.stub(WriteStream.prototype, 'getColorDepth').returns(8) 14 | ct.equal(depth.getColorDepth(), 8) 15 | 16 | stub.restore() 17 | ct.end() 18 | }) 19 | 20 | t.test('falls back to process.env.TERM when getColorDepth throws an error (deno scenario)', (ct) => { 21 | const stub = sinon.stub(WriteStream.prototype, 'getColorDepth').throws(new TypeError('Not a function')) 22 | 23 | process.env.TERM = 'xterm-256color' 24 | 25 | ct.equal(depth.getColorDepth(), 8, 'should return 8 since TERM is 256') 26 | 27 | stub.restore() 28 | ct.end() 29 | }) 30 | 31 | t.test('falls back to 4 when no process.env.TERM when getColorDepth throws an error (deno scenario)', (ct) => { 32 | const stub = sinon.stub(WriteStream.prototype, 'getColorDepth').throws(new TypeError('Not a function')) 33 | 34 | ct.equal(depth.getColorDepth(), 4, 'should return 4 since TERM is missing') 35 | 36 | stub.restore() 37 | ct.end() 38 | }) 39 | 40 | t.test('falls back to 4 when process.env.TERM neither xterm or 256color when getColorDepth throws an error (deno scenario)', (ct) => { 41 | process.env.TERM = 'something else' 42 | 43 | const stub = sinon.stub(WriteStream.prototype, 'getColorDepth').throws(new TypeError('Not a function')) 44 | 45 | ct.equal(depth.getColorDepth(), 4, 'should return 4 since TERM is missing') 46 | 47 | stub.restore() 48 | ct.end() 49 | }) 50 | -------------------------------------------------------------------------------- /tests/lib/helpers/conventions.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const conventions = require('../../../src/lib/helpers/conventions') 4 | 5 | t.beforeEach((ct) => { 6 | // important, clear process.env before each test 7 | process.env = {} 8 | }) 9 | 10 | t.test('#conventions', ct => { 11 | const envs = conventions('nextjs') 12 | 13 | ct.same(envs, [ 14 | { type: 'envFile', value: '.env.development.local' }, 15 | { type: 'envFile', value: '.env.local' }, 16 | { type: 'envFile', value: '.env.development' }, 17 | { type: 'envFile', value: '.env' } 18 | ]) 19 | 20 | ct.end() 21 | }) 22 | 23 | t.test('#conventions flow', ct => { 24 | const envs = conventions('flow') 25 | 26 | ct.same(envs, [ 27 | { type: 'envFile', value: '.env.development.local' }, 28 | { type: 'envFile', value: '.env.development' }, 29 | { type: 'envFile', value: '.env.local' }, 30 | { type: 'envFile', value: '.env' }, 31 | { type: 'envFile', value: '.env.defaults' } 32 | ]) 33 | 34 | ct.end() 35 | }) 36 | 37 | t.test('#conventions (invalid)', ct => { 38 | try { 39 | conventions('invalid') 40 | 41 | ct.fail('should have raised an error but did not') 42 | } catch (error) { 43 | const exampleError = new Error('INVALID_CONVENTION: \'invalid\'. permitted conventions: [\'nextjs\', \'flow\']') 44 | 45 | ct.same(error, exampleError) 46 | } 47 | 48 | ct.end() 49 | }) 50 | 51 | t.test('#conventions (process.env.NODE_ENV is test)', ct => { 52 | process.env.NODE_ENV = 'test' 53 | 54 | const envs = conventions('nextjs') 55 | 56 | ct.same(envs, [ 57 | { type: 'envFile', value: '.env.test.local' }, 58 | { type: 'envFile', value: '.env.test' }, 59 | { type: 'envFile', value: '.env' } 60 | ]) 61 | 62 | ct.end() 63 | }) 64 | 65 | t.test('#conventions (process.env.DOTENV_ENV is test)', ct => { 66 | process.env.DOTENV_ENV = 'test' 67 | 68 | const envs = conventions('nextjs') 69 | 70 | ct.same(envs, [ 71 | { type: 'envFile', value: '.env.test.local' }, 72 | { type: 'envFile', value: '.env.test' }, 73 | { type: 'envFile', value: '.env' } 74 | ]) 75 | 76 | ct.end() 77 | }) 78 | 79 | t.test('#conventions flow (process.env.NODE_ENV is test)', ct => { 80 | process.env.NODE_ENV = 'test' 81 | 82 | const envs = conventions('flow') 83 | 84 | ct.same(envs, [ 85 | { type: 'envFile', value: '.env.test.local' }, 86 | { type: 'envFile', value: '.env.test' }, 87 | { type: 'envFile', value: '.env.local' }, 88 | { type: 'envFile', value: '.env' }, 89 | { type: 'envFile', value: '.env.defaults' } 90 | ]) 91 | 92 | ct.end() 93 | }) 94 | 95 | t.test('#conventions (process.env.NODE_ENV is unrecognized)', ct => { 96 | process.env.NODE_ENV = 'unrecognized' 97 | 98 | const envs = conventions('nextjs') 99 | 100 | ct.same(envs, [ 101 | { type: 'envFile', value: '.env.local' }, 102 | { type: 'envFile', value: '.env' } 103 | ]) 104 | 105 | ct.end() 106 | }) 107 | 108 | t.test('#conventions flow (process.env.NODE_ENV is unrecognized)', ct => { 109 | process.env.NODE_ENV = 'unrecognized' 110 | 111 | const envs = conventions('flow') 112 | 113 | ct.same(envs, [ 114 | { type: 'envFile', value: '.env.unrecognized.local' }, 115 | { type: 'envFile', value: '.env.unrecognized' }, 116 | { type: 'envFile', value: '.env.local' }, 117 | { type: 'envFile', value: '.env' }, 118 | { type: 'envFile', value: '.env.defaults' } 119 | ]) 120 | 121 | ct.end() 122 | }) 123 | -------------------------------------------------------------------------------- /tests/lib/helpers/decrypt.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const sinon = require('sinon') 3 | const dotenv = require('dotenv') 4 | 5 | const decrypt = require('../../../src/lib/helpers/decrypt') 6 | const encrypt = require('../../../src/lib/helpers/encrypt') 7 | 8 | t.test('#decrypt', ct => { 9 | const dotenvKey = 'dotenv://:key_ac300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development' 10 | 11 | const ciphertext = encrypt('HELLO=World', dotenvKey) 12 | const decrypted = decrypt(ciphertext, dotenvKey) 13 | 14 | ct.same(decrypted, 'HELLO=World') 15 | 16 | ct.end() 17 | }) 18 | 19 | t.test('#decrypt (DECRYPTION_FAILED)', ct => { 20 | const dotenvKey = 'dotenv://:key_ac300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development' 21 | 22 | const ciphertext = encrypt('HELLO=World', dotenvKey) 23 | 24 | const badDotenvKey = 'dotenv://:key_bc300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development' 25 | 26 | try { 27 | decrypt(ciphertext, badDotenvKey) 28 | ct.fail('should have raised an error but did not') 29 | } catch (error) { 30 | ct.same(error.code, 'DECRYPTION_FAILED') 31 | ct.same(error.message, '[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.') 32 | ct.same(error.help, '[DECRYPTION_FAILED] Run with debug flag [dotenvx run --debug -- yourcommand] or manually run [echo $DOTENV_KEY] to compare it to the one in .env.keys.') 33 | ct.same(error.debug, '[DECRYPTION_FAILED] DOTENV_KEY is dotenv://:key_bc300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development') 34 | } 35 | 36 | ct.end() 37 | }) 38 | 39 | t.test('#decrypt (INVALID_CIPHERTEXT)', ct => { 40 | const dotenvKey = 'dotenv://:key_ac300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development' 41 | const ciphertext = 'abc' 42 | 43 | try { 44 | decrypt(ciphertext, dotenvKey) 45 | ct.fail('should have raised an error but did not') 46 | } catch (error) { 47 | ct.same(error.code, 'INVALID_CIPHERTEXT') 48 | ct.same(error.message, '[INVALID_CIPHERTEXT] Unable to decrypt what appears to be invalid ciphertext.') 49 | ct.same(error.help, '[INVALID_CIPHERTEXT] Run with debug flag [dotenvx run --debug -- yourcommand] or manually check .env.vault.') 50 | ct.same(error.debug, '[INVALID_CIPHERTEXT] ciphertext is \'abc\'') 51 | } 52 | 53 | ct.end() 54 | }) 55 | 56 | t.test('#decrypt (other error)', ct => { 57 | const dotenvKey = 'dotenv://:key_ac300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development' 58 | 59 | const ciphertext = encrypt('HELLO=World', dotenvKey) 60 | 61 | const decryptStub = sinon.stub(dotenv, 'decrypt').throws(new Error('other error')) 62 | 63 | try { 64 | decrypt(ciphertext, dotenvKey) 65 | ct.fail('should have raised an error but did not') 66 | } catch (error) { 67 | ct.same(error.message, 'other error') 68 | } 69 | 70 | decryptStub.restore() 71 | 72 | ct.end() 73 | }) 74 | -------------------------------------------------------------------------------- /tests/lib/helpers/detectEncoding.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const path = require('path') 3 | 4 | const detectEncoding = require('../../../src/lib/helpers/detectEncoding') 5 | 6 | t.test('#detectEncoding utf8', ct => { 7 | const filepath = path.resolve(__dirname, '../../', '.env') 8 | 9 | ct.equal(detectEncoding(filepath), 'utf8', 'Correctly detected utf8 encoding') 10 | 11 | ct.end() 12 | }) 13 | 14 | t.test('#detectEncoding utf16le', ct => { 15 | const filepath = path.resolve(__dirname, '../../', '.env.utf16le') 16 | 17 | ct.equal(detectEncoding(filepath), 'utf16le', 'Correctly detected utf16le encoding') 18 | 19 | ct.end() 20 | }) 21 | 22 | t.test('#detectEncoding fallback utf8 (latin1)', ct => { 23 | const filepath = path.resolve(__dirname, '../../', '.env.latin1') 24 | 25 | ct.equal(detectEncoding(filepath), 'utf8', 'Correctly fellback to utf8 encoding') 26 | 27 | ct.end() 28 | }) 29 | -------------------------------------------------------------------------------- /tests/lib/helpers/encryptValue.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const publicKey = '02b106c30579baf896ae1fddf077cbcb4fef5e7d457932974878dcb51f42b45498' 4 | const privateKey = '1fc1cafa954a7a2bf0a6fbff46189c9e03e3a66b4d1133108ab9fcdb9e154b70' 5 | 6 | const encryptValue = require('../../../src/lib/helpers/encryptValue') 7 | const decryptKeyValue = require('../../../src/lib/helpers/decryptKeyValue') 8 | 9 | t.test('#encryptValue', ct => { 10 | const result = encryptValue('hello', publicKey) 11 | ct.ok(result.startsWith('encrypted:')) 12 | 13 | const decrypted = decryptKeyValue('KEY', result, 'DOTENV_PRIVATE_KEY', privateKey) 14 | ct.same(decrypted, 'hello') 15 | 16 | ct.end() 17 | }) 18 | 19 | t.test('#encryptValue - implicit newlines', ct => { 20 | const value = `line 1 21 | line 2 22 | line 3` 23 | 24 | const result = encryptValue(value, publicKey) 25 | ct.ok(result.startsWith('encrypted:')) 26 | 27 | const decrypted = decryptKeyValue('KEY', result, 'DOTENV_PRIVATE_KEY', privateKey) 28 | ct.same(decrypted, value) 29 | 30 | ct.end() 31 | }) 32 | 33 | t.test('#encryptValue - explicit newlines', ct => { 34 | const value = 'line 1\nline 2\nline 3' 35 | 36 | const result = encryptValue(value, publicKey) 37 | ct.ok(result.startsWith('encrypted:')) 38 | 39 | const decrypted = decryptKeyValue('KEY', result, 'DOTENV_PRIVATE_KEY', privateKey) 40 | ct.same(decrypted, value) 41 | 42 | ct.end() 43 | }) 44 | -------------------------------------------------------------------------------- /tests/lib/helpers/errors.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const Errors = require('../../../src/lib/helpers/errors') 4 | 5 | t.test('#errors', ct => { 6 | const result = new Errors({ message: 'hi' }).dangerousDependencyHoist() 7 | 8 | t.equal(result.code, 'DANGEROUS_DEPENDENCY_HOIST') 9 | t.equal(result.message, '[DANGEROUS_DEPENDENCY_HOIST] your environment has hoisted an incompatible version of a dotenvx dependency: hi') 10 | t.equal(result.help, '[DANGEROUS_DEPENDENCY_HOIST] https://github.com/dotenvx/dotenvx/issues/622') 11 | 12 | ct.end() 13 | }) 14 | -------------------------------------------------------------------------------- /tests/lib/helpers/executeDynamic.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const sinon = require('sinon') 3 | const childProcess = require('child_process') 4 | 5 | const { logger } = require('../../../src/shared/logger') 6 | 7 | const executeDynamic = require('../../../src/lib/helpers/executeDynamic') 8 | 9 | const program = { 10 | outputHelp: sinon.stub() 11 | } 12 | 13 | t.beforeEach((ct) => { 14 | sinon.restore() 15 | }) 16 | 17 | t.test('executeDynamic - no command', ct => { 18 | const processExitStub = sinon.stub(process, 'exit') 19 | 20 | executeDynamic(program, undefined, []) 21 | 22 | ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1') 23 | 24 | ct.end() 25 | }) 26 | 27 | t.test('executeDynamic - pro command missing', ct => { 28 | const spawnSyncStub = sinon.stub(childProcess, 'spawnSync') 29 | const mockResult = { 30 | status: 1, 31 | error: new Error('Mock Error') 32 | } 33 | spawnSyncStub.returns(mockResult) 34 | const processExitStub = sinon.stub(process, 'exit') 35 | const consoleLogStub = sinon.stub(console, 'log') 36 | 37 | executeDynamic(program, 'pro', ['pro']) 38 | 39 | ct.ok(spawnSyncStub.called, 'spawnSync') 40 | ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1') 41 | ct.ok(consoleLogStub.called, 'console.log') 42 | 43 | ct.end() 44 | }) 45 | 46 | t.test('executeDynamic - other command missing', ct => { 47 | const spawnSyncStub = sinon.stub(childProcess, 'spawnSync') 48 | const mockResult = { 49 | status: 1, 50 | error: new Error('Mock Error') 51 | } 52 | spawnSyncStub.returns(mockResult) 53 | const processExitStub = sinon.stub(process, 'exit') 54 | const loggerHelpStub = sinon.stub(logger, 'help') 55 | const loggerWarnStub = sinon.stub(logger, 'warn') 56 | const loggerInfoStub = sinon.stub(logger, 'info') 57 | 58 | executeDynamic(program, 'other', ['other']) 59 | 60 | ct.ok(spawnSyncStub.called, 'spawnSync') 61 | ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1') 62 | ct.ok(loggerWarnStub.notCalled, 'warn') 63 | ct.ok(loggerHelpStub.notCalled, 'help') 64 | ct.ok(loggerInfoStub.calledWith('error: unknown command \'other\''), 'info') 65 | 66 | ct.end() 67 | }) 68 | 69 | t.test('executeDynamic - pro found', ct => { 70 | const spawnSyncStub = sinon.stub(childProcess, 'spawnSync') 71 | const mockResult = { 72 | status: 0 73 | } 74 | spawnSyncStub.returns(mockResult) 75 | const processExitStub = sinon.stub(process, 'exit') 76 | const loggerHelpStub = sinon.stub(logger, 'help') 77 | const loggerWarnStub = sinon.stub(logger, 'warn') 78 | 79 | executeDynamic(program, 'pro', ['pro']) 80 | 81 | ct.ok(spawnSyncStub.called, 'spawnSync') 82 | ct.ok(processExitStub.notCalled, 'process.exit should not be called') 83 | ct.ok(loggerWarnStub.notCalled, 'warn') 84 | ct.ok(loggerHelpStub.notCalled, 'help') 85 | 86 | ct.end() 87 | }) 88 | -------------------------------------------------------------------------------- /tests/lib/helpers/executeExtension.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const sinon = require('sinon') 3 | const childProcess = require('child_process') 4 | 5 | const { logger } = require('../../../src/shared/logger') 6 | 7 | const executeExtension = require('../../../src/lib/helpers/executeExtension') 8 | 9 | const ext = { 10 | outputHelp: sinon.stub() 11 | } 12 | 13 | t.beforeEach((ct) => { 14 | sinon.restore() 15 | }) 16 | 17 | t.test('executeExtension - no command', ct => { 18 | const processExitStub = sinon.stub(process, 'exit') 19 | 20 | executeExtension(ext, undefined, []) 21 | 22 | ct.ok(processExitStub.calledWith(0), 'process.exit should be called with code 0 to output help command') 23 | 24 | ct.end() 25 | }) 26 | 27 | t.test('executeExtension - vault command missing', ct => { 28 | const spawnSyncStub = sinon.stub(childProcess, 'spawnSync') 29 | const mockResult = { 30 | status: 1, 31 | error: new Error('Mock Error') 32 | } 33 | spawnSyncStub.returns(mockResult) 34 | const processExitStub = sinon.stub(process, 'exit') 35 | const loggerHelpStub = sinon.stub(logger, 'help') 36 | const loggerWarnStub = sinon.stub(logger, 'warn') 37 | 38 | executeExtension(ext, 'vault', ['vault']) 39 | 40 | ct.ok(spawnSyncStub.called, 'spawnSync') 41 | ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1') 42 | ct.ok(loggerWarnStub.calledWith('[INSTALLATION_NEEDED] install dotenvx-ext-vault to use [dotenvx ext vault] commands'), 'warn') 43 | ct.ok(loggerHelpStub.calledWith('? see installation instructions [https://github.com/dotenvx/dotenvx-ext-vault]'), 'help') 44 | 45 | ct.end() 46 | }) 47 | 48 | t.test('executeExtension - other command missing', ct => { 49 | const spawnSyncStub = sinon.stub(childProcess, 'spawnSync') 50 | const mockResult = { 51 | status: 1, 52 | error: new Error('Mock Error') 53 | } 54 | spawnSyncStub.returns(mockResult) 55 | const processExitStub = sinon.stub(process, 'exit') 56 | const loggerHelpStub = sinon.stub(logger, 'help') 57 | const loggerWarnStub = sinon.stub(logger, 'warn') 58 | const loggerInfoStub = sinon.stub(logger, 'info') 59 | 60 | executeExtension(ext, 'other', ['other']) 61 | 62 | ct.ok(spawnSyncStub.called, 'spawnSync') 63 | ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1') 64 | ct.ok(loggerWarnStub.notCalled, 'warn') 65 | ct.ok(loggerHelpStub.notCalled, 'help') 66 | ct.ok(loggerInfoStub.calledWith('error: unknown command \'other\''), 'info') 67 | 68 | ct.end() 69 | }) 70 | 71 | t.test('executeExtension - vault found', ct => { 72 | const spawnSyncStub = sinon.stub(childProcess, 'spawnSync') 73 | const mockResult = { 74 | status: 0 75 | } 76 | spawnSyncStub.returns(mockResult) 77 | const processExitStub = sinon.stub(process, 'exit') 78 | const loggerHelpStub = sinon.stub(logger, 'help') 79 | const loggerWarnStub = sinon.stub(logger, 'warn') 80 | 81 | executeExtension(ext, 'vault', ['vault']) 82 | 83 | ct.ok(spawnSyncStub.called, 'spawnSync') 84 | ct.ok(processExitStub.notCalled, 'process.exit should not be called') 85 | ct.ok(loggerWarnStub.notCalled, 'warn') 86 | ct.ok(loggerHelpStub.notCalled, 'help') 87 | 88 | ct.end() 89 | }) 90 | -------------------------------------------------------------------------------- /tests/lib/helpers/findEnvFiles.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const fsx = require('../../../src/lib/helpers/fsx') 3 | const sinon = require('sinon') 4 | 5 | const findEnvFiles = require('../../../src/lib/helpers/findEnvFiles') 6 | 7 | t.test('#findEnvFiles', ct => { 8 | const files = findEnvFiles('.') 9 | 10 | ct.same(files, []) 11 | 12 | ct.end() 13 | }) 14 | 15 | t.test('#findEnvFiles (tests/monorepo/apps/frontend)', ct => { 16 | const files = findEnvFiles('tests/monorepo/apps/frontend') 17 | 18 | ct.same(files, ['.env']) 19 | 20 | ct.end() 21 | }) 22 | 23 | t.test('#findEnvFiles (bad directory)', ct => { 24 | try { 25 | findEnvFiles('tests/does/not/exist') 26 | ct.fail('should have raised an error but did not') 27 | } catch (e) { 28 | ct.same(e.message, 'missing directory (tests/does/not/exist)') 29 | ct.same(e.code, 'MISSING_DIRECTORY') 30 | } 31 | 32 | ct.end() 33 | }) 34 | 35 | t.test('#findEnvFiles (other error)', ct => { 36 | const mockError = new Error('Mock Error') 37 | mockError.code = 'other' 38 | 39 | const stub = sinon.stub(fsx, 'readdirSync').throws(mockError) 40 | 41 | try { 42 | findEnvFiles('.') 43 | ct.fail('should have raised an error but did not') 44 | } catch (e) { 45 | ct.same(e.message, 'Mock Error') 46 | } 47 | 48 | stub.restore() 49 | 50 | ct.end() 51 | }) 52 | -------------------------------------------------------------------------------- /tests/lib/helpers/findPrivateKey.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const { findPrivateKey } = require('../../../src/lib/helpers/findPrivateKey') 3 | 4 | t.test('#findPrivateKey', ct => { 5 | const envFilepath = 'tests/monorepo/apps/encrypted/.env' 6 | const privateKey = findPrivateKey(envFilepath) 7 | 8 | t.equal(privateKey, 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1') 9 | 10 | ct.end() 11 | }) 12 | 13 | t.test('#findPrivateKey non-standard .env name (secrets.txt)', ct => { 14 | const envFilepath = 'tests/monorepo/apps/encrypted/secrets.txt' 15 | const privateKey = findPrivateKey(envFilepath) 16 | 17 | t.equal(privateKey, 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1') 18 | 19 | ct.end() 20 | }) 21 | 22 | t.test('#findPrivateKey non-standard .env name with no matching private key (secrets.ci.txt)', ct => { 23 | const envFilepath = 'tests/monorepo/apps/encrypted/secrets.ci.txt' 24 | const privateKey = findPrivateKey(envFilepath) 25 | 26 | t.equal(privateKey, null) 27 | 28 | ct.end() 29 | }) 30 | -------------------------------------------------------------------------------- /tests/lib/helpers/fsx.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const fs = require('fs') 3 | const sinon = require('sinon') 4 | 5 | const fsx = require('../../../src/lib/helpers/fsx') 6 | 7 | let writeFileSyncStub 8 | 9 | t.beforeEach((ct) => { 10 | process.env = {} 11 | writeFileSyncStub = sinon.stub(fs, 'writeFileSync') 12 | }) 13 | 14 | t.afterEach((ct) => { 15 | writeFileSyncStub.restore() 16 | }) 17 | 18 | t.test('#writeFileX', ct => { 19 | fsx.writeFileX('tests/somefile.txt') 20 | 21 | t.ok(writeFileSyncStub.called, 'fs.writeFileSync() called') 22 | 23 | ct.end() 24 | }) 25 | -------------------------------------------------------------------------------- /tests/lib/helpers/guessEnvironment.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const guessEnvironment = require('../../../src/lib/helpers/guessEnvironment') 4 | 5 | t.test('#guessEnvironment (.env)', ct => { 6 | const filepath = '.env' 7 | const environment = guessEnvironment(filepath) 8 | 9 | ct.same(environment, 'development') 10 | 11 | ct.end() 12 | }) 13 | 14 | t.test('#guessEnvironment (.env.production)', ct => { 15 | const filepath = '.env.production' 16 | const environment = guessEnvironment(filepath) 17 | 18 | ct.same(environment, 'production') 19 | 20 | ct.end() 21 | }) 22 | 23 | t.test('#guessEnvironment (.env.local)', (ct) => { 24 | const filepath = '.env.local' 25 | const environment = guessEnvironment(filepath) 26 | 27 | ct.same(environment, 'local') 28 | 29 | ct.end() 30 | }) 31 | 32 | t.test('#guessEnvironment (.env.development.local)', (ct) => { 33 | const filepath = '.env.development.local' 34 | const environment = guessEnvironment(filepath) 35 | 36 | ct.same(environment, 'development_local') 37 | 38 | ct.end() 39 | }) 40 | 41 | t.test('#guessEnvironment (.env.development.production)', (ct) => { 42 | const filepath = '.env.development.production' 43 | const environment = guessEnvironment(filepath) 44 | 45 | ct.same(environment, 'development_production') 46 | 47 | ct.end() 48 | }) 49 | 50 | t.test('#guessEnvironment (.env.some.other.thing)', (ct) => { 51 | const filepath = '.env.some.other.thing' 52 | const environment = guessEnvironment(filepath) 53 | 54 | ct.same(environment, 'some_other') 55 | 56 | ct.end() 57 | }) 58 | 59 | t.test('#guessEnvironment (.env1)', ct => { 60 | const filepath = '.env1' 61 | const environment = guessEnvironment(filepath) 62 | 63 | ct.same(environment, 'development1') 64 | 65 | ct.end() 66 | }) 67 | 68 | t.test('#guessEnvironment (secrets.txt)', ct => { 69 | const filepath = 'secrets.txt' 70 | const environment = guessEnvironment(filepath) 71 | 72 | ct.same(environment, 'secrets.txt') // for now just return the filename (might change in the future depending on user usage) 73 | 74 | ct.end() 75 | }) 76 | -------------------------------------------------------------------------------- /tests/lib/helpers/guessPrivateKeyFilename.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const guessPrivateKeyFilename = require('../../../src/lib/helpers/guessPrivateKeyFilename') 4 | 5 | t.test('#guessPrivateKeyFilename (DOTENV_PRIVATE_KEY)', ct => { 6 | const result = guessPrivateKeyFilename('DOTENV_PRIVATE_KEY') 7 | 8 | ct.same(result, '.env') 9 | 10 | ct.end() 11 | }) 12 | 13 | t.test('#guessPrivateKeyFilename (DOTENV_PRIVATE_KEY_PRODUCTION)', ct => { 14 | const result = guessPrivateKeyFilename('DOTENV_PRIVATE_KEY_PRODUCTION') 15 | 16 | ct.same(result, '.env.production') 17 | 18 | ct.end() 19 | }) 20 | 21 | t.test('#guessPrivateKeyFilename (DOTENV_PRIVATE_KEY_CI)', ct => { 22 | const result = guessPrivateKeyFilename('DOTENV_PRIVATE_KEY_CI') 23 | 24 | ct.same(result, '.env.ci') 25 | 26 | ct.end() 27 | }) 28 | 29 | t.test('#guessPrivateKeyFilename (DOTENV_PRIVATE_KEY_DEVELOPMENT_LOCAL)', ct => { 30 | const result = guessPrivateKeyFilename('DOTENV_PRIVATE_KEY_DEVELOPMENT_LOCAL') 31 | 32 | ct.same(result, '.env.development.local') 33 | 34 | ct.end() 35 | }) 36 | 37 | t.test('#guessPrivateKeyFilename (DOTENV_PRIVATE_KEY_DEVELOPMENT_LOCAL_ME)', ct => { 38 | const result = guessPrivateKeyFilename('DOTENV_PRIVATE_KEY_DEVELOPMENT_LOCAL_ME') 39 | 40 | ct.same(result, '.env.development.local.me') 41 | 42 | ct.end() 43 | }) 44 | -------------------------------------------------------------------------------- /tests/lib/helpers/guessPrivateKeyName.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const guessPrivateKeyName = require('../../../src/lib/helpers/guessPrivateKeyName') 4 | 5 | t.test('#guessPrivateKeyName (.env)', ct => { 6 | const filepath = '.env' 7 | const result = guessPrivateKeyName(filepath) 8 | 9 | ct.same(result, 'DOTENV_PRIVATE_KEY') 10 | 11 | ct.end() 12 | }) 13 | 14 | t.test('#guessPrivateKeyName (.env)', ct => { 15 | const filepath = 'some/path/to/.env' 16 | const result = guessPrivateKeyName(filepath) 17 | 18 | ct.same(result, 'DOTENV_PRIVATE_KEY') 19 | 20 | ct.end() 21 | }) 22 | 23 | t.test('#guessPrivateKeyName (.env.env)', ct => { 24 | const filepath = '.env.env' 25 | const result = guessPrivateKeyName(filepath) 26 | 27 | ct.same(result, 'DOTENV_PRIVATE_KEY_ENV') 28 | 29 | ct.end() 30 | }) 31 | 32 | t.test('#guessPrivateKeyName (.env.production)', ct => { 33 | const filepath = '.env.production' 34 | const result = guessPrivateKeyName(filepath) 35 | 36 | ct.same(result, 'DOTENV_PRIVATE_KEY_PRODUCTION') 37 | 38 | ct.end() 39 | }) 40 | 41 | t.test('#guessPrivateKeyName (.env.local)', (ct) => { 42 | const filepath = '.env.local' 43 | const result = guessPrivateKeyName(filepath) 44 | 45 | ct.same(result, 'DOTENV_PRIVATE_KEY_LOCAL') 46 | 47 | ct.end() 48 | }) 49 | 50 | t.test('#guessPrivateKeyName (.env.development.local)', (ct) => { 51 | const filepath = '.env.development.local' 52 | const result = guessPrivateKeyName(filepath) 53 | 54 | ct.same(result, 'DOTENV_PRIVATE_KEY_DEVELOPMENT_LOCAL') 55 | 56 | ct.end() 57 | }) 58 | 59 | t.test('#guessPrivateKeyName (.env.development.production)', (ct) => { 60 | const filepath = '.env.development.production' 61 | const result = guessPrivateKeyName(filepath) 62 | 63 | ct.same(result, 'DOTENV_PRIVATE_KEY_DEVELOPMENT_PRODUCTION') 64 | 65 | ct.end() 66 | }) 67 | 68 | t.test('#guessPrivateKeyName (.env.some.other.thing)', (ct) => { 69 | const filepath = '.env.some.other.thing' 70 | const result = guessPrivateKeyName(filepath) 71 | 72 | ct.same(result, 'DOTENV_PRIVATE_KEY_SOME_OTHER') 73 | 74 | ct.end() 75 | }) 76 | -------------------------------------------------------------------------------- /tests/lib/helpers/guessPublicKeyName.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const guessPublicKeyName = require('../../../src/lib/helpers/guessPublicKeyName') 4 | 5 | t.test('#guessPublicKeyName (.env)', ct => { 6 | const filepath = '.env' 7 | const result = guessPublicKeyName(filepath) 8 | 9 | ct.same(result, 'DOTENV_PUBLIC_KEY') 10 | 11 | ct.end() 12 | }) 13 | 14 | t.test('#guessPublicKeyName (.env)', ct => { 15 | const filepath = 'some/path/to/.env' 16 | const result = guessPublicKeyName(filepath) 17 | 18 | ct.same(result, 'DOTENV_PUBLIC_KEY') 19 | 20 | ct.end() 21 | }) 22 | 23 | t.test('#guessPublicKeyName (.env.env)', ct => { 24 | const filepath = '.env.env' 25 | const result = guessPublicKeyName(filepath) 26 | 27 | ct.same(result, 'DOTENV_PUBLIC_KEY_ENV') 28 | 29 | ct.end() 30 | }) 31 | 32 | t.test('#guessPublicKeyName (.env.production)', ct => { 33 | const filepath = '.env.production' 34 | const result = guessPublicKeyName(filepath) 35 | 36 | ct.same(result, 'DOTENV_PUBLIC_KEY_PRODUCTION') 37 | 38 | ct.end() 39 | }) 40 | 41 | t.test('#guessPublicKeyName (.env.local)', (ct) => { 42 | const filepath = '.env.local' 43 | const result = guessPublicKeyName(filepath) 44 | 45 | ct.same(result, 'DOTENV_PUBLIC_KEY_LOCAL') 46 | 47 | ct.end() 48 | }) 49 | 50 | t.test('#guessPublicKeyName (.env.development.local)', (ct) => { 51 | const filepath = '.env.development.local' 52 | const result = guessPublicKeyName(filepath) 53 | 54 | ct.same(result, 'DOTENV_PUBLIC_KEY_DEVELOPMENT_LOCAL') 55 | 56 | ct.end() 57 | }) 58 | 59 | t.test('#guessPublicKeyName (.env.development.production)', (ct) => { 60 | const filepath = '.env.development.production' 61 | const result = guessPublicKeyName(filepath) 62 | 63 | ct.same(result, 'DOTENV_PUBLIC_KEY_DEVELOPMENT_PRODUCTION') 64 | 65 | ct.end() 66 | }) 67 | 68 | t.test('#guessPublicKeyName (.env.some.other.thing)', (ct) => { 69 | const filepath = '.env.some.other.thing' 70 | const result = guessPublicKeyName(filepath) 71 | 72 | ct.same(result, 'DOTENV_PUBLIC_KEY_SOME_OTHER') 73 | 74 | ct.end() 75 | }) 76 | -------------------------------------------------------------------------------- /tests/lib/helpers/installPrecommitHook.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const fsx = require('../../../src/lib/helpers/fsx') 3 | const sinon = require('sinon') 4 | 5 | const InstallPrecommitHook = require('../../../src/lib/helpers/installPrecommitHook') 6 | 7 | t.test('#run (exists and already includes dotenvx ext precommit) does nothing', ct => { 8 | const installPrecommitHook = new InstallPrecommitHook() 9 | 10 | const existsStub = sinon.stub(installPrecommitHook, '_exists') 11 | const currentHookStub = sinon.stub(installPrecommitHook, '_currentHook') 12 | 13 | existsStub.returns(true) 14 | currentHookStub.returns('dotenvx ext precommit') 15 | 16 | const { successMessage } = installPrecommitHook.run() 17 | 18 | ct.same(successMessage, 'dotenvx ext precommit exists [.git/hooks/pre-commit]') 19 | 20 | // restore stubs 21 | existsStub.restore() 22 | currentHookStub.restore() 23 | 24 | ct.end() 25 | }) 26 | 27 | t.test('#run (exists but does not include dotenvx ext precommit) appends', ct => { 28 | const installPrecommitHook = new InstallPrecommitHook() 29 | 30 | const existsStub = sinon.stub(installPrecommitHook, '_exists') 31 | const currentHookStub = sinon.stub(installPrecommitHook, '_currentHook') 32 | const appendFileSyncStub = sinon.stub(fsx, 'appendFileSync') 33 | 34 | existsStub.returns(true) 35 | currentHookStub.returns('') // empty file 36 | 37 | const { successMessage } = installPrecommitHook.run() 38 | 39 | ct.same(successMessage, 'dotenvx ext precommit appended [.git/hooks/pre-commit]') 40 | 41 | t.ok(appendFileSyncStub.called, 'fsx.appendFileSync should be called') 42 | 43 | // restore stubs 44 | existsStub.restore() 45 | currentHookStub.restore() 46 | appendFileSyncStub.restore() 47 | 48 | ct.end() 49 | }) 50 | 51 | t.test('#run (does not exist) creates', ct => { 52 | const installPrecommitHook = new InstallPrecommitHook() 53 | 54 | const existsStub = sinon.stub(installPrecommitHook, '_exists') 55 | const writeFileXStub = sinon.stub(fsx, 'writeFileX') 56 | const chmodSyncStub = sinon.stub(fsx, 'chmodSync') 57 | 58 | existsStub.returns(false) 59 | 60 | const { successMessage } = installPrecommitHook.run() 61 | 62 | ct.same(successMessage, 'dotenvx ext precommit installed [.git/hooks/pre-commit]') 63 | 64 | t.ok(writeFileXStub.called, 'fsx.writeFileX should be called') 65 | t.ok(chmodSyncStub.called, 'fsx.chomdSyncStub should be called') 66 | 67 | // restore stubs 68 | existsStub.restore() 69 | writeFileXStub.restore() 70 | chmodSyncStub.restore() 71 | 72 | ct.end() 73 | }) 74 | 75 | t.test('#run (fs throws an error) logs error', ct => { 76 | const installPrecommitHook = new InstallPrecommitHook() 77 | 78 | const existsStub = sinon.stub(installPrecommitHook, '_exists') 79 | const writeFileXStub = sinon.stub(fsx, 'writeFileX').throws(new Error('Mock Error')) 80 | 81 | existsStub.returns(false) 82 | 83 | try { 84 | installPrecommitHook.run() 85 | ct.fail('should have raised an error but did not') 86 | } catch (error) { 87 | ct.same(error.message, 'failed to modify pre-commit hook: Mock Error') 88 | } 89 | 90 | // restore stubs 91 | existsStub.restore() 92 | writeFileXStub.restore() 93 | 94 | ct.end() 95 | }) 96 | 97 | t.test('#_exists true/false', ct => { 98 | const installPrecommitHook = new InstallPrecommitHook() 99 | 100 | const existsSyncStub = sinon.stub(fsx, 'existsSync') 101 | 102 | existsSyncStub.returns(false) 103 | let result = installPrecommitHook._exists() 104 | ct.equal(result, false) 105 | 106 | existsSyncStub.returns(true) 107 | result = installPrecommitHook._exists() 108 | ct.equal(result, true) 109 | 110 | existsSyncStub.restore() 111 | 112 | ct.end() 113 | }) 114 | 115 | t.test('#_currentHook', ct => { 116 | const installPrecommitHook = new InstallPrecommitHook() 117 | 118 | const readFileXStub = sinon.stub(fsx, 'readFileX') 119 | 120 | readFileXStub.returns('some file') 121 | const result = installPrecommitHook._currentHook() 122 | ct.equal(result, 'some file') 123 | 124 | readFileXStub.restore() 125 | 126 | ct.end() 127 | }) 128 | -------------------------------------------------------------------------------- /tests/lib/helpers/isEncrypted.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const isEncrypted = require('../../../src/lib/helpers/isEncrypted') 3 | 4 | t.test('#isEncrypted', ct => { 5 | const result = isEncrypted('encrypted:1234') 6 | ct.same(result, true) 7 | ct.end() 8 | }) 9 | 10 | t.test('#isEncrypted real world', ct => { 11 | const result = isEncrypted('encrypted:BHqJQhgpCHY4My3PQ1UE3vSTyfqM/wNaISWSVQgi8eBQze4X7AMkcl0tg4skow5vI7Akhm0UXV43+FeYOvxcXifjjKbHZeXp+hFxKk5zu/N3tB95DiCpXSLA2bbcyeTNLAfTZgXa') 12 | ct.same(result, true) 13 | ct.end() 14 | }) 15 | 16 | t.test('#isEncrypted not encrypted', ct => { 17 | const result = isEncrypted('1234') 18 | ct.same(result, false) 19 | ct.end() 20 | }) 21 | 22 | t.test('#isEncrypted passes null', ct => { 23 | const result = isEncrypted(null) 24 | ct.same(result, false) 25 | ct.end() 26 | }) 27 | -------------------------------------------------------------------------------- /tests/lib/helpers/isFullyEncrypted.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const isFullyEncrypted = require('../../../src/lib/helpers/isFullyEncrypted') 3 | 4 | t.test('#isFullyEncrypted - All values are encrypted', ct => { 5 | const str = ` 6 | # .env 7 | HELLO=encrypted:1234 8 | `.trim() 9 | 10 | const result = isFullyEncrypted(str) 11 | ct.same(result, true, 'All values are encrypted') 12 | ct.end() 13 | }) 14 | 15 | t.test('#isFullyEncrypted - No values are encrypted', ct => { 16 | const str = ` 17 | # .env 18 | HELLO=World 19 | `.trim() 20 | 21 | const result = isFullyEncrypted(str) 22 | ct.same(result, false, 'No values are encrypted') 23 | ct.end() 24 | }) 25 | 26 | t.test('#isFullyEncrypted - partially encrypted and partially unencrypted', ct => { 27 | const str = ` 28 | # .env 29 | HELLO=unencrypted 30 | HELLO=encrypted:1234 31 | `.trim() 32 | 33 | const result = isFullyEncrypted(str) 34 | ct.same(result, false, 'Some values are unencrypted') 35 | ct.end() 36 | }) 37 | 38 | t.test('#isFullyEncrypted - Encrypted values with DOTENV_PUBLIC_KEY', ct => { 39 | const str = ` 40 | #/-------------------[DOTENV_PUBLIC_KEY]--------------------/ 41 | #/ public-key encryption for .env files / 42 | #/ [how it works](https://dotenvx.com/encryption) / 43 | #/----------------------------------------------------------/ 44 | DOTENV_PUBLIC_KEY="0395dc734661dfd7a2d6581cd2c8864038028c2570f6586771534525767341d1b2" 45 | 46 | # .env 47 | HELLO="encrypted:BFYjx93CX4d4OIRzYMR+ZT+JR92kCfOJSsivsXxwaQHvA5FJgHa50rUHWhj1t72LLeRkLE2v4GrKpW5w1bjinXEmXtAV28k2audEVW6cBU7YapLVcPvrV2FkNqbMEKRvp78C0wKaMvNarg==" 48 | `.trim() 49 | 50 | const result = isFullyEncrypted(str) 51 | ct.same(result, true, 'Encrypted values with DOTENV_PUBLIC_KEY') 52 | ct.end() 53 | }) 54 | 55 | t.test('#isFullyEncrypted - Keys starting with DOTENV_PUBLIC_KEY are considered valid', ct => { 56 | const str = ` 57 | # .env 58 | DOTENV_PUBLIC_KEY_ENVIRONMENT="somevalue" 59 | HELLO="encrypted:BFYjx93CX4d4OIRzYMR+ZT+JR92kCfOJSsivsXxwaQHvA5FJgHa50rUHWhj1t72LLeRkLE2v4GrKpW5w1bjinXEmXtAV28k2audEVW6cBU7YapLVcPvrV2FkNqbMEKRvp78C0wKaMvNarg==" 60 | `.trim() 61 | 62 | const result = isFullyEncrypted(str) 63 | ct.same(result, true, 'Keys starting with DOTENV_PUBLIC_KEY are considered valid') 64 | ct.end() 65 | }) 66 | -------------------------------------------------------------------------------- /tests/lib/helpers/isIgnoringDotenvKeys.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const fsx = require('../../../src/lib/helpers/fsx') 3 | const sinon = require('sinon') 4 | 5 | const isIgnoringDotenvKeys = require('../../../src/lib/helpers/isIgnoringDotenvKeys') 6 | 7 | t.test('#isIgnoringDotenvKeys - no .gitignore file', ct => { 8 | const existsSyncStub = sinon.stub(fsx, 'existsSync') 9 | existsSyncStub.returns(false) 10 | 11 | const result = isIgnoringDotenvKeys() 12 | ct.same(result, false) 13 | 14 | existsSyncStub.restore() 15 | 16 | ct.end() 17 | }) 18 | 19 | t.test('#isIgnoringDotenvKeys - empty .gitignore file', ct => { 20 | const existsSyncStub = sinon.stub(fsx, 'existsSync') 21 | existsSyncStub.returns(true) 22 | const readFileXStub = sinon.stub(fsx, 'readFileX') 23 | readFileXStub.returns('') 24 | 25 | const result = isIgnoringDotenvKeys() 26 | 27 | ct.same(result, false) 28 | 29 | existsSyncStub.restore() 30 | readFileXStub.restore() 31 | 32 | ct.end() 33 | }) 34 | 35 | t.test('#isIgnoringDotenvKeys - .gitignore file ignores .env*', ct => { 36 | const existsSyncStub = sinon.stub(fsx, 'existsSync') 37 | existsSyncStub.returns(true) 38 | const readFileXStub = sinon.stub(fsx, 'readFileX') 39 | readFileXStub.returns('.env*') 40 | 41 | const result = isIgnoringDotenvKeys() 42 | 43 | ct.same(result, true) 44 | 45 | existsSyncStub.restore() 46 | readFileXStub.restore() 47 | 48 | ct.end() 49 | }) 50 | -------------------------------------------------------------------------------- /tests/lib/helpers/isPublicKey.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const isPublicKey = require('../../../src/lib/helpers/isPublicKey') 3 | 4 | t.test('#isPublicKey not encrypted but DOTENV_PUBLIC_KEY', ct => { 5 | const result = isPublicKey('DOTENV_PUBLIC_KEY', '1234') 6 | ct.same(result, true) 7 | ct.end() 8 | }) 9 | 10 | t.test('#isPublicKey not encrypted but DOTENV_PUBLIC_KEY_PRODUCTION', ct => { 11 | const result = isPublicKey('DOTENV_PUBLIC_KEY_PRODUCTION', '1234') 12 | ct.same(result, true) 13 | ct.end() 14 | }) 15 | -------------------------------------------------------------------------------- /tests/lib/helpers/keypair.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const keypair = require('../../../src/lib/helpers/keypair') 3 | 4 | t.test('#keypair', ct => { 5 | const { publicKey, privateKey } = keypair() 6 | 7 | t.ok(publicKey, 'Public key should be defined') 8 | t.ok(privateKey, 'Private key should be defined') 9 | t.equal(publicKey.length, 66, 'Public key should be 66 characters long') 10 | t.equal(privateKey.length, 64, 'Private key should be 64 characters long') 11 | 12 | ct.end() 13 | }) 14 | 15 | t.test('keypair uses provided private key to generate public key', (t) => { 16 | const existingPrivateKey = '4c06b1f9ffc4af11d0d206fd43f28bc96b68647158c1666edc4832f19857cef9' 17 | const { publicKey, privateKey } = keypair(existingPrivateKey) 18 | 19 | t.equal(privateKey, existingPrivateKey, 'Private key should match the provided private key') 20 | t.ok(publicKey, 'Public key should be defined') 21 | t.equal(publicKey.length, 66, 'Public key should be 66 characters long') 22 | 23 | // Generate the public key from the provided private key for comparison 24 | const { PrivateKey } = require('eciesjs') 25 | const kp = new PrivateKey(Buffer.from(existingPrivateKey, 'hex')) 26 | const expectedPublicKey = kp.publicKey.toHex() 27 | 28 | t.equal(publicKey, expectedPublicKey, 'Public key should match the expected public key generated from the provided private key') 29 | 30 | t.end() 31 | }) 32 | -------------------------------------------------------------------------------- /tests/lib/helpers/parseEncryptionKeyFromDotenvKey.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const parseEncryptionKeyFromDotenvKey = require('../../../src/lib/helpers/parseEncryptionKeyFromDotenvKey') 4 | 5 | t.test('#parseEncryptionKeyFromDotenvKey', ct => { 6 | const dotenvKey = 'dotenv://:key_e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34@dotenvx.com/vault/.env.vault?environment=other' 7 | 8 | const key = parseEncryptionKeyFromDotenvKey(dotenvKey) // buffer hex 9 | const expected = Buffer.from('e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34', 'hex') 10 | 11 | ct.same(key, expected) 12 | 13 | ct.end() 14 | }) 15 | 16 | t.test('#parseEncryptionKeyFromDotenvKey (not url parseable)', ct => { 17 | const dotenvKey = 'e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34' 18 | 19 | try { 20 | parseEncryptionKeyFromDotenvKey(dotenvKey) // buffer hex 21 | 22 | ct.fail('should have raised an error but did not') 23 | } catch (error) { 24 | const exampleError = new Error('INVALID_DOTENV_KEY: Incomplete format. It should be a dotenv uri. (dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development)') 25 | 26 | ct.same(error, exampleError) 27 | } 28 | 29 | ct.end() 30 | }) 31 | 32 | t.test('#parseEncryptionKeyFromDotenvKey (missing key/password part)', ct => { 33 | const dotenvKey = 'dotenv://:@dotenvx.com/vault/.env.vault?environment=other' 34 | 35 | try { 36 | parseEncryptionKeyFromDotenvKey(dotenvKey) // buffer hex 37 | 38 | ct.fail('should have raised an error but did not') 39 | } catch (error) { 40 | const exampleError = new Error('INVALID_DOTENV_KEY: Missing key part') 41 | 42 | ct.same(error, exampleError) 43 | } 44 | 45 | ct.end() 46 | }) 47 | -------------------------------------------------------------------------------- /tests/lib/helpers/parseEnvironmentFromDotenvKey.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const parseEnvironmentFromDotenvKey = require('../../../src/lib/helpers/parseEnvironmentFromDotenvKey') 4 | 5 | t.test('#parseEnvironmentFromDotenvKey', ct => { 6 | const dotenvKey = 'dotenv://:key_e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34@dotenvx.com/vault/.env.vault?environment=development' 7 | 8 | const environment = parseEnvironmentFromDotenvKey(dotenvKey) 9 | 10 | ct.same(environment, 'development') 11 | 12 | ct.end() 13 | }) 14 | 15 | t.test('#parseEnvironmentFromDotenvKey (not url parseable)', ct => { 16 | const dotenvKey = 'e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34' 17 | 18 | try { 19 | parseEnvironmentFromDotenvKey(dotenvKey) 20 | 21 | ct.fail('should have raised an error but did not') 22 | } catch (error) { 23 | const exampleError = new Error('INVALID_DOTENV_KEY: Invalid URL') 24 | 25 | ct.same(error, exampleError) 26 | } 27 | 28 | ct.end() 29 | }) 30 | 31 | t.test('#parseEnvironmentFromDotenvKey (missing environment part)', ct => { 32 | const dotenvKey = 'dotenv://:key_e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34@dotenvx.com/vault/.env.vault?environment=' 33 | 34 | try { 35 | parseEnvironmentFromDotenvKey(dotenvKey) 36 | 37 | ct.fail('should have raised an error but did not') 38 | } catch (error) { 39 | const exampleError = new Error('INVALID_DOTENV_KEY: Missing environment part') 40 | 41 | ct.same(error, exampleError) 42 | } 43 | 44 | ct.end() 45 | }) 46 | -------------------------------------------------------------------------------- /tests/lib/helpers/pluralize.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const pluralize = require('../../../src/lib/helpers/pluralize') 4 | 5 | t.test('#pluralize', ct => { 6 | const result0 = pluralize('world', 0) 7 | const result1 = pluralize('world', 1) 8 | const result2 = pluralize('world', 2) 9 | 10 | ct.same(result0, 'worlds') 11 | ct.same(result1, 'world') 12 | ct.same(result2, 'worlds') 13 | 14 | ct.end() 15 | }) 16 | -------------------------------------------------------------------------------- /tests/lib/helpers/proKeypair.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const sinon = require('sinon') 3 | const childProcess = require('child_process') 4 | 5 | const ProKeypair = require('../../../src/lib/helpers/proKeypair') 6 | 7 | t.test('#proKeypair', ct => { 8 | const keypairs = new ProKeypair('tests/monorepo/apps/app1/.env').run() 9 | 10 | ct.same(keypairs, { DOTENV_PUBLIC_KEY: null, DOTENV_PRIVATE_KEY: null }) 11 | 12 | ct.end() 13 | }) 14 | 15 | t.test('#proKeypair when childProcess fails', ct => { 16 | const stub = sinon.stub(childProcess, 'execSync').throws(new Error('Command failed')) 17 | 18 | const keypairs = new ProKeypair('tests/monorepo/apps/app1/.env').run() 19 | 20 | ct.same(keypairs, { DOTENV_PUBLIC_KEY: null, DOTENV_PRIVATE_KEY: null }) 21 | 22 | stub.restore() 23 | 24 | ct.end() 25 | }) 26 | -------------------------------------------------------------------------------- /tests/lib/helpers/removeDynamicHelpSection.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const removeDynamicHelpSection = require('../../../src/lib/helpers/removeDynamicHelpSection') 4 | 5 | t.test('#removeDynamicHelpSection', ct => { 6 | const lines = [ 7 | 'Usage: dotenvx [options] [command] [command] [args...]', 8 | '', 9 | 'a better dotenv–from the creator of `dotenv`', 10 | '', 11 | 'Arguments:', 12 | ' command dynamic command', 13 | ' args dynamic command arguments', 14 | '', 15 | 'Options:' 16 | ] 17 | 18 | removeDynamicHelpSection(lines) 19 | 20 | ct.same(lines, [ 21 | 'Usage: dotenvx [options] [command] [command] [args...]', 22 | '', 23 | 'a better dotenv–from the creator of `dotenv`', 24 | '', 25 | 'Options:' 26 | ]) 27 | 28 | ct.end() 29 | }) 30 | -------------------------------------------------------------------------------- /tests/lib/helpers/removeOptionsHelpParts.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const removeOptionsHelpParts = require('../../../src/lib/helpers/removeOptionsHelpParts') 4 | 5 | t.test('#removeOptionsHelpParts', ct => { 6 | const lines = [ 7 | 'set [options] <KEY> <value> set a single environment variable' 8 | ] 9 | 10 | removeOptionsHelpParts(lines) 11 | 12 | ct.same(lines, [ 13 | 'set <KEY> <value> set a single environment variable' 14 | ]) 15 | 16 | ct.end() 17 | }) 18 | -------------------------------------------------------------------------------- /tests/lib/helpers/smartDotenvPrivateKey.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const smartDotenvPrivateKey = require('../../../src/lib/helpers/smartDotenvPrivateKey') 4 | 5 | t.beforeEach((ct) => { 6 | // important, clear process.env before each test 7 | process.env = {} 8 | }) 9 | 10 | let filepath = '.env' 11 | 12 | t.test('#smartDotenvPrivateKey', ct => { 13 | const result = smartDotenvPrivateKey(filepath) 14 | 15 | ct.same(result, null) 16 | 17 | ct.end() 18 | }) 19 | 20 | t.test('#smartDotenvPrivateKey when process.env.DOTENV_PRIVATE_KEY is set', ct => { 21 | process.env.DOTENV_PRIVATE_KEY = '<privateKey>' 22 | 23 | const result = smartDotenvPrivateKey(filepath) 24 | 25 | ct.same(result, '<privateKey>') 26 | 27 | ct.end() 28 | }) 29 | 30 | t.test('#smartDotenvPrivateKey when process.env.DOTENV_PRIVATE_KEY is set but it is an empty string', ct => { 31 | process.env.DOTENV_PRIVATE_KEY = '' 32 | 33 | const result = smartDotenvPrivateKey(filepath) 34 | 35 | ct.same(result, null) 36 | 37 | ct.end() 38 | }) 39 | 40 | t.test('#smartDotenvPrivateKey when .env.keys present', ct => { 41 | filepath = 'tests/monorepo/apps/encrypted/.env' 42 | 43 | const result = smartDotenvPrivateKey(filepath) 44 | 45 | ct.same(result, 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1') 46 | 47 | ct.end() 48 | }) 49 | 50 | t.test('#smartDotenvPrivateKey when .env.keys present but filename is not .env (use invertse of DOTENV_PUBLIC_KEY* in the filename (if exists))', ct => { 51 | filepath = 'tests/monorepo/apps/encrypted/secrets.txt' 52 | 53 | const result = smartDotenvPrivateKey(filepath) 54 | 55 | ct.same(result, 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1') 56 | 57 | ct.end() 58 | }) 59 | 60 | t.test('#smartDotenvPrivateKey when DOTENV_PRIVATE_KEY passed and custom filename', ct => { 61 | process.env.DOTENV_PRIVATE_KEY = '<privateKey>' 62 | 63 | filepath = 'tests/monorepo/apps/encrypted/secrets.txt' 64 | 65 | const result = smartDotenvPrivateKey(filepath) 66 | 67 | ct.same(result, '<privateKey>') 68 | 69 | ct.end() 70 | }) 71 | 72 | t.test('#smartDotenvPrivateKey when DOTENV_PRIVATE_KEY passed and custom filename but custom filename has a different named DOTENV_PUBLIC_KEY', ct => { 73 | process.env.DOTENV_PRIVATE_KEY = '<privateKey>' 74 | 75 | filepath = 'tests/monorepo/apps/encrypted/secrets.ci.txt' 76 | 77 | const result = smartDotenvPrivateKey(filepath) 78 | ct.same(result, null) // it should not find it because it is instead looking for a DOTENV_PRIVATE_KEY_CI (see secrets.ci.txt contents) 79 | 80 | // matching ci key is set - inverse of the ci public key in the secrets.ci.txt file 81 | process.env.DOTENV_PRIVATE_KEY_CI = '<privateKeyCi>' 82 | const result2 = smartDotenvPrivateKey(filepath) 83 | ct.same(result2, '<privateKeyCi>') 84 | 85 | // an additional random key is set but still with the dotenv private key schema 86 | process.env.DOTENV_PRIVATE_KEY_PRODUCTION = '<privateKeyProduction>' 87 | const result3 = smartDotenvPrivateKey(filepath) 88 | ct.same(result3, '<privateKeyCi>') // it should still find the CI one first 89 | 90 | ct.end() 91 | }) 92 | -------------------------------------------------------------------------------- /tests/lib/helpers/smartDotenvPublicKey.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const smartDotenvPublicKey = require('../../../src/lib/helpers/smartDotenvPublicKey') 4 | 5 | t.beforeEach((ct) => { 6 | // important, clear process.env before each test 7 | process.env = {} 8 | }) 9 | 10 | let filepath = '.env' 11 | 12 | t.test('#smartDotenvPublicKey', ct => { 13 | const result = smartDotenvPublicKey(filepath) 14 | 15 | ct.same(result, null) 16 | 17 | ct.end() 18 | }) 19 | 20 | t.test('#smartDotenvPublicKey when process.env.DOTENV_PUBLIC_KEY is set', ct => { 21 | process.env.DOTENV_PUBLIC_KEY = '<publicKey>' 22 | 23 | const result = smartDotenvPublicKey(filepath) 24 | 25 | ct.same(result, '<publicKey>') 26 | 27 | ct.end() 28 | }) 29 | 30 | t.test('#smartDotenvPublicKey when process.env.DOTENV_PUBLIC_KEY is set but it is an empty string', ct => { 31 | process.env.DOTENV_PUBLIC_KEY = '' 32 | 33 | const result = smartDotenvPublicKey(filepath) 34 | 35 | ct.same(result, null) 36 | 37 | ct.end() 38 | }) 39 | 40 | t.test('#smartDotenvPublicKey when .env.keys present', ct => { 41 | filepath = 'tests/monorepo/apps/encrypted/.env' 42 | 43 | const result = smartDotenvPublicKey(filepath) 44 | 45 | ct.same(result, '03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba') 46 | 47 | ct.end() 48 | }) 49 | -------------------------------------------------------------------------------- /tests/lib/helpers/truncate.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const truncate = require('../../../src/lib/helpers/truncate') 4 | 5 | t.test('#truncate', ct => { 6 | const privateKey = '2c93601cba85b3b2474817897826ebef977415c097f0bf57dcbaa3056e5d64d0' 7 | 8 | const result = truncate(privateKey) 9 | 10 | t.equal(result, '2c93601…') 11 | 12 | ct.end() 13 | }) 14 | 15 | t.test('#truncate - 11 characters', ct => { 16 | const privateKey = 'dxo_123456789' 17 | 18 | const result = truncate(privateKey, 11) 19 | 20 | t.equal(result, 'dxo_1234567…') 21 | 22 | ct.end() 23 | }) 24 | 25 | t.test('#truncate - null privateKey', ct => { 26 | const privateKey = null 27 | 28 | const result = truncate(privateKey) 29 | 30 | t.equal(result, '') 31 | 32 | ct.end() 33 | }) 34 | -------------------------------------------------------------------------------- /tests/lib/services/genexample.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const fsx = require('../../../src/lib/helpers/fsx') 3 | const path = require('path') 4 | const sinon = require('sinon') 5 | 6 | const Genexample = require('../../../src/lib/services/genexample') 7 | 8 | t.test('#run', ct => { 9 | const genexample = new Genexample('tests/monorepo/apps/frontend') 10 | 11 | const { 12 | envExampleFile, 13 | injected, 14 | preExisted 15 | } = genexample.run() 16 | 17 | const output = `# .env.example - generated with dotenvx 18 | 19 | # for testing purposes only 20 | HELLO="" # this is a comment 21 | ` 22 | ct.same(envExampleFile, output) 23 | ct.same(injected, { HELLO: '' }) 24 | ct.same(preExisted, {}) 25 | 26 | ct.end() 27 | }) 28 | 29 | t.test('#run (.env.example already exists)', ct => { 30 | const genexample = new Genexample('tests/monorepo/apps/backend') 31 | 32 | const { 33 | envExampleFile, 34 | injected, 35 | preExisted 36 | } = genexample.run() 37 | 38 | const output = `# .env.example - generated with dotenvx 39 | HELLO='' 40 | ` 41 | ct.same(envExampleFile, output) 42 | ct.same(injected, {}) 43 | ct.same(preExisted, { HELLO: '' }) 44 | 45 | ct.end() 46 | }) 47 | 48 | t.test('#run (.env.example already exists but with different keys)', ct => { 49 | const originalReadFileSync = fsx.readFileX 50 | const sandbox = sinon.createSandbox() 51 | sandbox.stub(fsx, 'readFileX').callsFake((filepath, options) => { 52 | if (filepath === path.resolve('tests/monorepo/apps/backend/.env')) { 53 | return 'HELLO=world\nHELLO2=universe' 54 | } else { 55 | return originalReadFileSync(filepath, options) 56 | } 57 | }) 58 | 59 | const genexample = new Genexample('tests/monorepo/apps/backend') 60 | 61 | const { 62 | envExampleFile, 63 | injected, 64 | preExisted 65 | } = genexample.run() 66 | 67 | const output = `# .env.example - generated with dotenvx 68 | HELLO='' 69 | HELLO2='' 70 | ` 71 | 72 | ct.same(envExampleFile, output) 73 | ct.same(injected, { HELLO2: '' }) 74 | ct.same(preExisted, { HELLO: '' }) 75 | 76 | ct.end() 77 | }) 78 | 79 | t.test('#run (string envFile)', ct => { 80 | const genexample = new Genexample('tests/monorepo/apps/frontend', '.env') 81 | 82 | const { 83 | envExampleFile, 84 | injected, 85 | preExisted 86 | } = genexample.run() 87 | 88 | const output = `# .env.example - generated with dotenvx 89 | 90 | # for testing purposes only 91 | HELLO="" # this is a comment 92 | ` 93 | ct.same(envExampleFile, output) 94 | ct.same(injected, { HELLO: '' }) 95 | ct.same(preExisted, {}) 96 | 97 | ct.end() 98 | }) 99 | 100 | t.test('#run (cant find directory)', ct => { 101 | try { 102 | new Genexample('tests/monorepo/apps/frontendzzzz').run() 103 | 104 | ct.fail('should have raised an error but did not') 105 | } catch (error) { 106 | ct.equal(error.message, 'missing directory (tests/monorepo/apps/frontendzzzz)') 107 | } 108 | 109 | ct.end() 110 | }) 111 | 112 | t.test('#run (missing env files)', ct => { 113 | try { 114 | new Genexample('tests/monorepo/apps/frontend', []).run() 115 | ct.fail('should have raised an error but did not') 116 | } catch (error) { 117 | ct.equal(error.code, 'MISSING_ENV_FILES') 118 | ct.equal(error.message, 'no .env* files found') 119 | ct.equal(error.help, '? add one with [echo "HELLO=World" > .env] and then run [dotenvx genexample]') 120 | } 121 | 122 | ct.end() 123 | }) 124 | 125 | t.test('#run (non-existent .env file)', ct => { 126 | try { 127 | new Genexample('tests/monorepo/apps/frontend', ['.env.nonexistent']).run() 128 | ct.fail('should have raised an error but did not') 129 | } catch (error) { 130 | ct.equal(error.code, 'MISSING_ENV_FILE') 131 | ct.equal(error.message, `[MISSING_ENV_FILE] missing .env.nonexistent file (${path.resolve('tests/monorepo/apps/frontend/.env.nonexistent')})`) 132 | ct.equal(error.help, '? add it with [echo "HELLO=World" > .env.nonexistent] and then run [dotenvx genexample]') 133 | } 134 | 135 | ct.end() 136 | }) 137 | -------------------------------------------------------------------------------- /tests/lib/services/get.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const Get = require('../../../src/lib/services/get') 4 | 5 | t.beforeEach((ct) => { 6 | // important, clear process.env before each test 7 | process.env = {} 8 | }) 9 | 10 | t.test('#run (missing key returns the entire processEnv as object)', ct => { 11 | const { parsed } = new Get().run() 12 | 13 | ct.same(parsed, {}) 14 | 15 | ct.end() 16 | }) 17 | 18 | t.test('#run (all object) with preset process.env', ct => { 19 | process.env.PRESET_ENV_EXAMPLE = 'something/on/machine' 20 | 21 | const { parsed } = new Get(null, [], false, '', true).run() 22 | ct.same(parsed, { PRESET_ENV_EXAMPLE: 'something/on/machine' }) 23 | 24 | const result = new Get(null, [], false, '', false).run() 25 | ct.same(result.parsed, {}) 26 | 27 | ct.end() 28 | }) 29 | 30 | t.test('#run (missing key returns the entire processEnv as object)', ct => { 31 | const envs = [ 32 | { type: 'envFile', value: 'tests/.env.local' } 33 | ] 34 | const { parsed } = new Get(null, envs).run() 35 | 36 | ct.same(parsed, { BASIC: 'local_basic', LOCAL: 'local' }) 37 | 38 | ct.end() 39 | }) 40 | 41 | t.test('#run (missing key returns empty string when fetching single key)', ct => { 42 | const envs = [ 43 | { type: 'envFile', value: 'tests/.env.local' } 44 | ] 45 | const { parsed } = new Get('BAZ', envs).run() 46 | 47 | ct.same(parsed.BAZ, undefined) 48 | 49 | ct.end() 50 | }) 51 | 52 | t.test('#run', ct => { 53 | const envs = [ 54 | { type: 'envFile', value: 'tests/.env' } 55 | ] 56 | const { parsed } = new Get('BASIC', envs).run() 57 | 58 | ct.same(parsed.BASIC, 'basic') 59 | 60 | ct.end() 61 | }) 62 | 63 | t.test('#run (as multi-array)', ct => { 64 | const envs = [ 65 | { type: 'envFile', value: 'tests/.env' }, 66 | { type: 'envFile', value: 'tests/.env.local' } 67 | ] 68 | const { parsed } = new Get('BASIC', envs).run() 69 | 70 | ct.same(parsed.BASIC, 'basic') 71 | 72 | ct.end() 73 | }) 74 | 75 | t.test('#run (as multi-array reversed (first wins))', ct => { 76 | const envs = [ 77 | { type: 'envFile', value: 'tests/.env.local' }, 78 | { type: 'envFile', value: 'tests/.env' } 79 | ] 80 | 81 | const { parsed } = new Get('BASIC', envs).run() 82 | 83 | ct.same(parsed.BASIC, 'local_basic') 84 | 85 | ct.end() 86 | }) 87 | 88 | t.test('#run (as multi-array reversed with overload (second wins))', ct => { 89 | const envs = [ 90 | { type: 'envFile', value: 'tests/.env.local' }, 91 | { type: 'envFile', value: 'tests/.env' } 92 | ] 93 | 94 | const { parsed } = new Get('BASIC', envs, true).run() 95 | 96 | ct.same(parsed.BASIC, 'basic') 97 | 98 | ct.end() 99 | }) 100 | 101 | t.test('#run (as multi-array - some not found)', ct => { 102 | const envs = [ 103 | { type: 'envFile', value: 'tests/.env.notfound' }, 104 | { type: 'envFile', value: 'tests/.env' } 105 | ] 106 | 107 | const { parsed } = new Get('BASIC', envs, true).run() 108 | 109 | ct.same(parsed.BASIC, 'basic') 110 | 111 | ct.end() 112 | }) 113 | 114 | t.test('#run (process.env already exists on machine)', ct => { 115 | process.env.BASIC = 'existing' 116 | 117 | const envs = [ 118 | { type: 'envFile', value: 'tests/.env.local' } 119 | ] 120 | 121 | const { parsed } = new Get('BASIC', envs).run() 122 | 123 | ct.same(parsed.BASIC, 'existing') 124 | 125 | ct.end() 126 | }) 127 | 128 | t.test('#run (no key and process.env already exists on machine)', ct => { 129 | process.env.BASIC = 'existing' 130 | 131 | const envs = [ 132 | { type: 'envFile', value: 'tests/.env.local' } 133 | ] 134 | 135 | const { parsed } = new Get(null, envs).run() 136 | 137 | ct.same(parsed, { BASIC: 'existing', LOCAL: 'local' }) 138 | 139 | ct.end() 140 | }) 141 | 142 | t.test('#run expansion', ct => { 143 | const envs = [ 144 | { type: 'envFile', value: 'tests/.env.expand' } 145 | ] 146 | 147 | const { parsed } = new Get('BASIC_EXPAND', envs).run() 148 | 149 | ct.same(parsed.BASIC_EXPAND, 'basic') 150 | 151 | ct.end() 152 | }) 153 | -------------------------------------------------------------------------------- /tests/lib/services/keypair.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const fs = require('fs') 3 | const sinon = require('sinon') 4 | 5 | const Keypair = require('../../../src/lib/services/keypair') 6 | 7 | let writeFileSyncStub 8 | 9 | t.beforeEach((ct) => { 10 | process.env = {} 11 | writeFileSyncStub = sinon.stub(fs, 'writeFileSync') 12 | }) 13 | 14 | t.afterEach((ct) => { 15 | writeFileSyncStub.restore() 16 | }) 17 | 18 | t.test('#run (no arguments)', ct => { 19 | const result = new Keypair().run() 20 | 21 | ct.same(result, { DOTENV_PUBLIC_KEY: null, DOTENV_PRIVATE_KEY: null }) 22 | 23 | ct.end() 24 | }) 25 | 26 | t.test('#run (finds .env file)', ct => { 27 | const envFile = 'tests/monorepo/apps/encrypted/.env' 28 | const result = new Keypair(envFile).run() 29 | 30 | ct.same(result, { DOTENV_PUBLIC_KEY: '03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba', DOTENV_PRIVATE_KEY: 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1' }) 31 | 32 | ct.end() 33 | }) 34 | 35 | t.test('#run (finds .env file as array)', ct => { 36 | const envFile = 'tests/monorepo/apps/encrypted/.env' 37 | const result = new Keypair([envFile]).run() 38 | 39 | ct.same(result, { DOTENV_PUBLIC_KEY: '03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba', DOTENV_PRIVATE_KEY: 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1' }) 40 | 41 | ct.end() 42 | }) 43 | -------------------------------------------------------------------------------- /tests/monorepo/.env.keys: -------------------------------------------------------------------------------- 1 | #/------------------!DOTENV_PRIVATE_KEYS!-------------------/ 2 | #/ private decryption keys. DO NOT commit to source control / 3 | #/ [how it works](https://dotenvx.com/encryption) / 4 | #/----------------------------------------------------------/ 5 | 6 | # .env.production 7 | DOTENV_PRIVATE_KEY_PRODUCTION="f96fbb9b3e99b46d891879414e5c800113f0e757adc0bb7fb73c152072de669b" 8 | -------------------------------------------------------------------------------- /tests/monorepo/apps/app1/.env: -------------------------------------------------------------------------------- 1 | # for testing purposes only 2 | HELLO="app1" 3 | -------------------------------------------------------------------------------- /tests/monorepo/apps/app1/.env.production: -------------------------------------------------------------------------------- 1 | # for testing purposes only 2 | HELLO="production" 3 | -------------------------------------------------------------------------------- /tests/monorepo/apps/backend/.env: -------------------------------------------------------------------------------- 1 | # for testing purposes only 2 | HELLO="backend" 3 | -------------------------------------------------------------------------------- /tests/monorepo/apps/backend/.env.example: -------------------------------------------------------------------------------- 1 | # .env.example - generated with dotenvx 2 | HELLO='' 3 | -------------------------------------------------------------------------------- /tests/monorepo/apps/backend/.env.keys: -------------------------------------------------------------------------------- 1 | #/!!!!!!!!!!!!!!!!!!!.env.keys!!!!!!!!!!!!!!!!!!!!!!/ 2 | #/ DOTENV_KEYs. DO NOT commit to source control / 3 | #/ [how it works](https://dotenvx.com/env-keys) / 4 | #/--------------------------------------------------/ 5 | DOTENV_KEY_DEVELOPMENT="dotenv://:key_e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34@dotenvx.com/vault/.env.vault?environment=development" 6 | -------------------------------------------------------------------------------- /tests/monorepo/apps/backend/.env.previous: -------------------------------------------------------------------------------- 1 | # for testing purposes only 2 | HELLO="previous" 3 | -------------------------------------------------------------------------------- /tests/monorepo/apps/backend/.env.untracked: -------------------------------------------------------------------------------- 1 | HELLO="untracked" 2 | -------------------------------------------------------------------------------- /tests/monorepo/apps/backend/.env.vault: -------------------------------------------------------------------------------- 1 | #/-------------------.env.vault---------------------/ 2 | #/ cloud-agnostic vaulting standard / 3 | #/ [how it works](https://dotenvx.com/env-vault) / 4 | #/--------------------------------------------------/ 5 | 6 | # development 7 | DOTENV_VAULT_DEVELOPMENT="TgaIyXmiLS1ej5LrII+Boz8R8nQ4avEM/pcreOfLUehTMmludeyXn6HMXLu8Jjn9O0yckjXy7kRrNfUvUJ88V8RpTwDP8k7u" 8 | -------------------------------------------------------------------------------- /tests/monorepo/apps/encrypted/.env: -------------------------------------------------------------------------------- 1 | #/-------------------[DOTENV_PUBLIC_KEY]--------------------/ 2 | #/ public-key encryption for .env files / 3 | #/ [how it works](https://dotenvx.com/encryption) / 4 | #/----------------------------------------------------------/ 5 | DOTENV_PUBLIC_KEY="03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba" 6 | 7 | # .env 8 | HELLO="encrypted:BG8M6U+GKJGwpGA42ml2erb9+T2NBX6Z2JkBLynDy21poz0UfF5aPxCgRbIyhnQFdWKd0C9GZ7lM5PeL86xghoMcWvvPpkyQ0yaD2pZ64RzoxFGB1lTZYlEgQOxTDJnWxODHfuQcFY10uA==" 9 | -------------------------------------------------------------------------------- /tests/monorepo/apps/encrypted/.env.keys: -------------------------------------------------------------------------------- 1 | #/------------------!DOTENV_PRIVATE_KEYS!-------------------/ 2 | #/ private decryption keys. DO NOT commit to source control / 3 | #/ [how it works](https://dotenvx.com/encryption) / 4 | #/----------------------------------------------------------/ 5 | 6 | # .env 7 | DOTENV_PRIVATE_KEY="ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1" 8 | -------------------------------------------------------------------------------- /tests/monorepo/apps/encrypted/secrets.ci.txt: -------------------------------------------------------------------------------- 1 | #/-------------------[DOTENV_PUBLIC_KEY]--------------------/ 2 | #/ public-key encryption for .env files / 3 | #/ [how it works](https://dotenvx.com/encryption) / 4 | #/----------------------------------------------------------/ 5 | DOTENV_PUBLIC_KEY_CI="03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba" 6 | 7 | # .env 8 | HELLO="encrypted:BG8M6U+GKJGwpGA42ml2erb9+T2NBX6Z2JkBLynDy21poz0UfF5aPxCgRbIyhnQFdWKd0C9GZ7lM5PeL86xghoMcWvvPpkyQ0yaD2pZ64RzoxFGB1lTZYlEgQOxTDJnWxODHfuQcFY10uA==" 9 | -------------------------------------------------------------------------------- /tests/monorepo/apps/encrypted/secrets.txt: -------------------------------------------------------------------------------- 1 | #/-------------------[DOTENV_PUBLIC_KEY]--------------------/ 2 | #/ public-key encryption for .env files / 3 | #/ [how it works](https://dotenvx.com/encryption) / 4 | #/----------------------------------------------------------/ 5 | DOTENV_PUBLIC_KEY="03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba" 6 | 7 | # .env 8 | HELLO="encrypted:BG8M6U+GKJGwpGA42ml2erb9+T2NBX6Z2JkBLynDy21poz0UfF5aPxCgRbIyhnQFdWKd0C9GZ7lM5PeL86xghoMcWvvPpkyQ0yaD2pZ64RzoxFGB1lTZYlEgQOxTDJnWxODHfuQcFY10uA==" 9 | -------------------------------------------------------------------------------- /tests/monorepo/apps/frontend/.env: -------------------------------------------------------------------------------- 1 | # for testing purposes only 2 | HELLO="frontend" # this is a comment 3 | -------------------------------------------------------------------------------- /tests/monorepo/apps/multiline/.env: -------------------------------------------------------------------------------- 1 | HELLO="-----BEGIN RSA PRIVATE KEY----- 2 | ABCD 3 | EFGH 4 | IJKL 5 | -----END RSA PRIVATE KEY-----" 6 | ALOHA="-----BEGIN RSA PRIVATE KEY-----\nABCD\nEFGH\nIJKL\n-----END RSA PRIVATE KEY-----" 7 | -------------------------------------------------------------------------------- /tests/monorepo/apps/multiline/.env.crlf: -------------------------------------------------------------------------------- 1 | HELLO="-----BEGIN RSA PRIVATE KEY----- 2 | ABCD 3 | EFGH 4 | IJKL 5 | -----END RSA PRIVATE KEY-----" 6 | ALOHA="-----BEGIN RSA PRIVATE KEY-----\r\nABCD\r\nEFGH\r\nIJKL\r\n-----END RSA PRIVATE KEY-----" 7 | -------------------------------------------------------------------------------- /tests/monorepo/apps/multiple/.env: -------------------------------------------------------------------------------- 1 | HELLO="one" 2 | HELLO2="two" 3 | HELLO3="three" 4 | -------------------------------------------------------------------------------- /tests/monorepo/apps/multiple/.env.keys: -------------------------------------------------------------------------------- 1 | #/------------------!DOTENV_PRIVATE_KEYS!-------------------/ 2 | #/ private decryption keys. DO NOT commit to source control / 3 | #/ [how it works](https://dotenvx.com/encryption) / 4 | #/----------------------------------------------------------/ 5 | 6 | # .env.production 7 | DOTENV_PRIVATE_KEY_PRODUCTION="f96fbb9b3e99b46d891879414e5c800113f0e757adc0bb7fb73c152072de669b" 8 | -------------------------------------------------------------------------------- /tests/monorepo/apps/multiple/.env.production: -------------------------------------------------------------------------------- 1 | #/-------------------[DOTENV_PUBLIC_KEY]--------------------/ 2 | #/ public-key encryption for .env files / 3 | #/ [how it works](https://dotenvx.com/encryption) / 4 | #/----------------------------------------------------------/ 5 | DOTENV_PUBLIC_KEY_PRODUCTION="027907f1e455c0e881f12e0e385461df4166dc6324d1a584ce47858485357e16bb" 6 | 7 | # .env.production 8 | HELLO="encrypted:BOYFQaf+RnJa5/Myo+PbeGpmKnqLQlcl7x1vnoGRHdzlHK9rDFjPpWC5g82If69OPEnbijj8ywWd8QUAdOXLHlYrf8+KGg/QF6AgpyNrMUzMhCHyHDnWilKlywHH677xj1wL9g==" 9 | HELLO2="encrypted:BDaopY2/icyl9MYMy7WNAdVYnCncLf0eLlloubA9Tki4VaLkiyK+Hj1nlIBAxGzjD/s0PkPIrfa/QXQdQwlHtsFYixikZhT8nDMdfV80cKKjKJJ7oANLFH1Ck+/VAW2tbv+tYQ==" 10 | HELLO3="encrypted:BF4XTKdeAwj4yESsmlFHAm7TIQ9rGK3qDVP0gMUKGSMZ8YX345NJ0QZtCWKVXl0+z+6XrjwlCQe3fSFsYV0H+SMJapMMmq2hIr1GqGX423XCkdS6oMCRpCqsbV0/w7yTEsBRirST" 11 | -------------------------------------------------------------------------------- /tests/monorepo/apps/shebang/.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | HELLO="shebang" 3 | -------------------------------------------------------------------------------- /tests/monorepo/apps/unencrypted/.env: -------------------------------------------------------------------------------- 1 | HELLO="unencrypted" 2 | -------------------------------------------------------------------------------- /tests/multiline.txt: -------------------------------------------------------------------------------- 1 | one 2 | two 3 | three 4 | -------------------------------------------------------------------------------- /tests/shared/colors.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const sinon = require('sinon') 3 | 4 | const { getColor, bold } = require('../../src/shared/colors') 5 | const depth = require('../../src/lib/helpers/colorDepth') 6 | 7 | t.test('getColor with ansi256 color support', (ct) => { 8 | const stub = sinon.stub(depth, 'getColorDepth').returns(8) 9 | 10 | ct.equal(getColor('orangered')('hello'), '\x1b[38;5;202mhello\x1b[39m') 11 | 12 | stub.restore() 13 | ct.end() 14 | }) 15 | 16 | t.test('getColor with ansi16 color support', (ct) => { 17 | const stub = sinon.stub(depth, 'getColorDepth').returns(4) 18 | 19 | ct.equal(getColor('orangered')('hello'), '\x1b[31mhello\x1b[39m') 20 | 21 | stub.restore() 22 | ct.end() 23 | }) 24 | 25 | t.test('getColor without color support', (ct) => { 26 | const stub = sinon.stub(depth, 'getColorDepth').returns(1) 27 | 28 | ct.equal(getColor('orangered')('hello'), 'hello') 29 | 30 | stub.restore() 31 | ct.end() 32 | }) 33 | 34 | t.test('getColor invalid color', (ct) => { 35 | try { 36 | getColor('invalid') 37 | 38 | ct.fail('getColor should throw error') 39 | } catch (error) { 40 | ct.pass(' threw an error') 41 | ct.equal(error.message, 'Invalid color invalid') 42 | } 43 | 44 | ct.end() 45 | }) 46 | 47 | t.test('bold with ansi16 color support', (ct) => { 48 | const stub = sinon.stub(depth, 'getColorDepth').returns(4) 49 | 50 | ct.equal(bold('hello'), '\x1b[1mhello\x1b[22m') 51 | 52 | stub.restore() 53 | ct.end() 54 | }) 55 | 56 | t.test('bold without color support', (ct) => { 57 | const stub = sinon.stub(depth, 'getColorDepth').returns(1) 58 | 59 | ct.equal(bold('hello'), 'hello') 60 | 61 | stub.restore() 62 | ct.end() 63 | }) 64 | --------------------------------------------------------------------------------