@dotenvx/dotenvx 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,6 +72,28 @@ More examples
72
72
  Hello World
73
73
  ```
74
74
 
75
+ </details>
76
+ * <details><summary>Java ☕️</summary><br>
77
+
78
+ ```sh
79
+ $ echo 'public class Index { public static void main(String[] args) { System.out.println("Hello " + System.getenv("HELLO")); } }' > index.java
80
+
81
+ $ dotenvx run -- java index.java
82
+ Hello World
83
+ ```
84
+
85
+ </details>
86
+ * <details><summary>.NET 🔵</summary><br>
87
+
88
+ ```sh
89
+ $ dotnet new console -n HelloWorld -o HelloWorld
90
+ $ cd HelloWorld
91
+ $ echo 'Console.WriteLine($"Hello {Environment.GetEnvironmentVariable("HELLO")}");' > Program.cs && echo "HELLO=World" > .env
92
+
93
+ $ dotenvx run -- dotnet run
94
+ Hello World
95
+ ```
96
+
75
97
  </details>
76
98
  * <details><summary>Frameworks ▲</summary><br>
77
99
 
@@ -124,6 +146,35 @@ More examples
124
146
  ```
125
147
 
126
148
  </details>
149
+ * <details><summary>npm</summary><br>
150
+
151
+ ```sh
152
+ $ npm install @dotenvx/dotenvx --save
153
+ ```
154
+
155
+ ```json
156
+ {
157
+ "scripts": {
158
+ "start": "./node_modules/.bin/dotenvx run -- node index.js"
159
+ },
160
+ "dependencies": {
161
+ "@dotenvx/dotenvx": "^0.5.0"
162
+ }
163
+ }
164
+ ```
165
+
166
+ ```sh
167
+ $ npm run start
168
+
169
+ > start
170
+ > ./node_modules/.bin/dotenvx run -- node index.js
171
+
172
+ [dotenvx][INFO] injecting 1 environment variable from .env
173
+ Hello World
174
+ ```
175
+
176
+ </details>
177
+
127
178
  * <details><summary>Git</summary><br>
128
179
 
129
180
  ```sh
@@ -143,14 +194,14 @@ Pass the `--env-file` flag (shorthand `-f`) to run any environment from a `.env.
143
194
 
144
195
  ```sh
145
196
  $ dotenvx run --env-file=.env.production -- node index.js
146
- [dotenvx][INFO] Injecting 12 production environment variables into your application process
197
+ [dotenvx][INFO] injecting 12 environment variables from .env.production
147
198
  ```
148
199
 
149
200
  Combine multiple `.env` files if you like.
150
201
 
151
202
  ```
152
203
  $ dotenvx run --env-file=.env.local --env-file=.env -- node index.js
153
- [dotenvx][INFO] Injecting 12 local, 1 development environment variables into your application process
204
+ [dotenvx][INFO] injecting 13 environment variables from .env.local,.env
154
205
  ```
155
206
 
156
207
  &nbsp;
@@ -158,7 +209,7 @@ $ dotenvx run --env-file=.env.local --env-file=.env -- node index.js
158
209
  ## Encrypt Your Env Files
159
210
 
160
211
  ```
161
- dotenvx encrypt
212
+ $ dotenvx encrypt
162
213
  ```
163
214
 
164
215
  > This will encrypt your `.env` file to a `.env.vault` file. Commit your `.env.vault` file safely to code.
@@ -188,7 +239,7 @@ Run it with `dotenvx`.
188
239
 
189
240
  ```sh
190
241
  $ dotenvx run -- node index.js
191
- [dotenvx@x.x.x][WARN] ENOENT: no such file or directory, open '/../../.env'
242
+ [dotenvx][WARN] ENOENT: no such file or directory, open '/../../.env'
192
243
  Hello undefined
193
244
  ```
194
245
 
@@ -205,7 +256,7 @@ Run it again.
205
256
 
206
257
  ```sh
207
258
  $ dotenvx run -- node index.js
208
- [dotenvx@x.x.x][INFO] Injecting 0 environment variables into your application process
259
+ [dotenvx][INFO] injecting 0 environment variables from .env
209
260
  Hello undefined
210
261
  ```
211
262
 
@@ -213,10 +264,10 @@ Hrm, still undefined. Pass the `--debug` flag to debug the issue. I'll give you
213
264
 
214
265
  ```sh
215
266
  $ dotenvx run --debug -- node index.js
216
- [dotenvx@x.x.x][VERBOSE] Loading env from /../../.env
217
- [dotenvx@x.x.x][DEBUG] Reading env from /../../.env
218
- [dotenvx@x.x.x][DEBUG] Parsing env from /../../.env
219
- [dotenvx@x.x.x][DEBUG] {"JELLO":"World"}
267
+ [dotenvx][VERBOSE] Loading env from /../../.env
268
+ [dotenvx][DEBUG] Reading env from /../../.env
269
+ [dotenvx][DEBUG] Parsing env from /../../.env
270
+ [dotenvx][DEBUG] {"JELLO":"World"}
220
271
 
221
272
  # Oops, HELLO not JELLO ^^
222
273
  ```
@@ -232,7 +283,7 @@ One last time. [Le tired](https://youtu.be/kCpjgl2baLs?t=45).
232
283
 
233
284
  ```sh
234
285
  $ dotenvx run -- node index.js
235
- [dotenvx@x.x.x][INFO] Injecting 1 environment variable into your application process
286
+ [dotenvx][INFO] injecting 1 environment variable from .env
236
287
  Hello World
237
288
  ```
238
289
 
@@ -269,7 +320,6 @@ npx @dotenvx/dotenvx help
269
320
  npm i @dotenvx/dotenvx --save
270
321
  ```
271
322
  ```json
272
- // package.json
273
323
  "scripts": {
274
324
  "start": "./node_modules/.bin/dotenvx run -- nodex index.js"
275
325
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.0",
2
+ "version": "0.6.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a better dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -21,7 +21,7 @@
21
21
  "scripts": {
22
22
  "standard": "standard",
23
23
  "standard:fix": "standard --fix",
24
- "test": "jest"
24
+ "test": "jest --verbose"
25
25
  },
26
26
  "dependencies": {
27
27
  "commander": "^11.1.0",
@@ -2,7 +2,6 @@
2
2
 
3
3
  const fs = require('fs')
4
4
  const { Command } = require('commander')
5
- const dotenv = require('dotenv')
6
5
  const program = new Command()
7
6
 
8
7
  // constants
@@ -10,6 +9,7 @@ const ENCODING = 'utf8'
10
9
 
11
10
  const logger = require('./../shared/logger')
12
11
  const helpers = require('./helpers')
12
+ const { AppendToIgnores } = require('./ignores')
13
13
  const packageJson = require('./../shared/packageJson')
14
14
  const main = require('./../lib/main')
15
15
 
@@ -20,6 +20,8 @@ program
20
20
  .option('-v, --verbose', 'sets log level to verbose')
21
21
  .option('-d, --debug', 'sets log level to debug')
22
22
  .hook('preAction', (thisCommand, actionCommand) => {
23
+ new AppendToIgnores().run()
24
+
23
25
  const options = thisCommand.opts()
24
26
 
25
27
  if (options.logLevel) {
@@ -59,40 +61,97 @@ program.command('run')
59
61
  logger.debug('configuring options')
60
62
  logger.debug(options)
61
63
 
62
- // convert to array if needed
63
- let optionEnvFile = options.envFile
64
- if (!Array.isArray(optionEnvFile)) {
65
- optionEnvFile = [optionEnvFile]
66
- }
64
+ // load from .env.vault file
65
+ if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
66
+ const filepath = helpers.resolvePath('.env.vault')
67
+
68
+ if (!fs.existsSync(filepath)) {
69
+ logger.error(`you set DOTENV_KEY but your .env.vault file is missing: ${filepath}`)
70
+ } else {
71
+ logger.verbose(`injecting encrypted env from ${filepath}`)
72
+
73
+ try {
74
+ logger.debug(`reading encrypted env from ${filepath}`)
75
+ const src = fs.readFileSync(filepath, { encoding: ENCODING })
76
+
77
+ logger.debug(`parsing encrypted env from ${filepath}`)
78
+ const parsedVault = main.parse(src)
79
+
80
+ logger.debug(`decrypting encrypted env from ${filepath}`)
81
+ // handle scenario for comma separated keys - for use with key rotation
82
+ // example: DOTENV_KEY="dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenv.org/vault/.env.vault?environment=prod"
83
+ const dotenvKeys = process.env.DOTENV_KEY.split(',')
84
+ const length = dotenvKeys.length
85
+
86
+ let decrypted
87
+ for (let i = 0; i < length; i++) {
88
+ try {
89
+ // Get full dotenvKey
90
+ const dotenvKey = dotenvKeys[i].trim()
91
+
92
+ const key = helpers._parseEncryptionKeyFromDotenvKey(dotenvKey)
93
+ const ciphertext = helpers._parseCipherTextFromDotenvKeyAndParsedVault(dotenvKey, parsedVault)
94
+
95
+ // Decrypt
96
+ decrypted = main.decrypt(ciphertext, key)
97
+
98
+ break
99
+ } catch (error) {
100
+ // last key
101
+ if (i + 1 >= length) {
102
+ throw error
103
+ }
104
+ // try next key
105
+ }
106
+ }
107
+ logger.debug(decrypted)
108
+
109
+ logger.debug(`parsing decrypted env from ${filepath}`)
110
+ const parsed = main.parse(decrypted)
111
+
112
+ logger.debug(`writing decrypted env from ${filepath}`)
113
+ const result = main.write(process.env, parsed, options.overload)
114
+
115
+ logger.info(`injecting ${result.written.size} environment ${helpers.pluralize('variable', result.written.size)} from encrypted .env.vault`)
116
+ } catch (e) {
117
+ logger.error(e)
118
+ }
119
+ }
120
+ } else {
121
+ // convert to array if needed
122
+ let optionEnvFile = options.envFile
123
+ if (!Array.isArray(optionEnvFile)) {
124
+ optionEnvFile = [optionEnvFile]
125
+ }
67
126
 
68
- const env = {}
69
- const readableFilepaths = new Set()
70
- const written = new Set()
127
+ const readableFilepaths = new Set()
128
+ const written = new Set()
71
129
 
72
- for (const envFilepath of optionEnvFile) {
73
- const filepath = helpers.resolvePath(envFilepath)
130
+ for (const envFilepath of optionEnvFile) {
131
+ const filepath = helpers.resolvePath(envFilepath)
74
132
 
75
- logger.verbose(`injecting env from ${filepath}`)
133
+ logger.verbose(`injecting env from ${filepath}`)
76
134
 
77
- try {
78
- logger.debug(`reading env from ${filepath}`)
79
- const src = fs.readFileSync(filepath, { encoding: ENCODING })
135
+ try {
136
+ logger.debug(`reading env from ${filepath}`)
137
+ const src = fs.readFileSync(filepath, { encoding: ENCODING })
80
138
 
81
- logger.debug(`parsing env from ${filepath}`)
82
- const parsed = main.parse(src)
139
+ logger.debug(`parsing env from ${filepath}`)
140
+ const parsed = main.parse(src)
83
141
 
84
- logger.debug(`writing env from ${filepath}`)
85
- const result = main.write(process.env, parsed, options.overload)
142
+ logger.debug(`writing env from ${filepath}`)
143
+ const result = main.write(process.env, parsed, options.overload)
86
144
 
87
- readableFilepaths.add(envFilepath)
88
- result.written.forEach(key => written.add(key))
89
- } catch (e) {
90
- logger.warn(e)
145
+ readableFilepaths.add(envFilepath)
146
+ result.written.forEach(key => written.add(key))
147
+ } catch (e) {
148
+ logger.warn(e)
149
+ }
91
150
  }
92
- }
93
151
 
94
- if (readableFilepaths.size > 0) {
95
- logger.info(`injecting ${written.size} environment ${helpers.pluralize('variable', written.size)} from ${[...readableFilepaths]}`)
152
+ if (readableFilepaths.size > 0) {
153
+ logger.info(`injecting ${written.size} environment ${helpers.pluralize('variable', written.size)} from ${[...readableFilepaths]}`)
154
+ }
96
155
  }
97
156
 
98
157
  // Extract command and arguments after '--'
@@ -103,7 +162,7 @@ program.command('run')
103
162
  } else {
104
163
  const subCommand = process.argv.slice(commandIndex + 1)
105
164
 
106
- helpers.executeCommand(subCommand, env)
165
+ helpers.executeCommand(subCommand, process.env)
107
166
  }
108
167
  })
109
168
 
@@ -124,7 +183,7 @@ program.command('encrypt')
124
183
  try {
125
184
  logger.verbose(`generating .env.keys from ${optionEnvFile}`)
126
185
 
127
- const dotenvKeys = (dotenv.configDotenv({ path: '.env.keys' }).parsed || {})
186
+ const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
128
187
 
129
188
  for (const envFilepath of optionEnvFile) {
130
189
  const filepath = helpers.resolvePath(envFilepath)
@@ -169,8 +228,8 @@ program.command('encrypt')
169
228
  try {
170
229
  logger.verbose(`generating .env.vault from ${optionEnvFile}`)
171
230
 
172
- const dotenvKeys = (dotenv.configDotenv({ path: '.env.keys' }).parsed || {})
173
- const dotenvVaults = (dotenv.configDotenv({ path: '.env.vault' }).parsed || {})
231
+ const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
232
+ const dotenvVaults = (main.configDotenv({ path: '.env.vault' }).parsed || {})
174
233
 
175
234
  for (const envFilepath of optionEnvFile) {
176
235
  const filepath = helpers.resolvePath(envFilepath)
@@ -211,8 +270,6 @@ program.command('encrypt')
211
270
  }
212
271
 
213
272
  logger.info(`encrypted ${optionEnvFile} to .env.vault`)
214
-
215
- // logger.info(`encrypting`)
216
273
  })
217
274
 
218
275
  program.parse(process.argv)
@@ -4,6 +4,7 @@ const crypto = require('crypto')
4
4
  const { spawn } = require('child_process')
5
5
  const xxhash = require('xxhashjs')
6
6
  const XXHASH_SEED = 0xABCD
7
+ const NONCE_BYTES = 12
7
8
 
8
9
  const main = require('./../lib/main')
9
10
 
@@ -69,17 +70,17 @@ const generateDotenvKey = function (environment) {
69
70
  }
70
71
 
71
72
  const encryptFile = function (filepath, dotenvKey, encoding) {
72
- const key = this._parseEncryptionKeyFromDotenvKey(dotenvKey)
73
+ const key = _parseEncryptionKeyFromDotenvKey(dotenvKey)
73
74
  const message = fs.readFileSync(filepath, encoding)
74
75
 
75
- const ciphertext = this.encrypt(key, message)
76
+ const ciphertext = encrypt(key, message)
76
77
 
77
78
  return ciphertext
78
79
  }
79
80
 
80
81
  const encrypt = function (key, message) {
81
82
  // set up nonce
82
- const nonce = this._generateNonce()
83
+ const nonce = crypto.randomBytes(NONCE_BYTES)
83
84
 
84
85
  // set up cipher
85
86
  const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
@@ -98,11 +99,11 @@ const encrypt = function (key, message) {
98
99
  }
99
100
 
100
101
  const changed = function (ciphertext, dotenvKey, filepath, encoding) {
101
- const key = this._parseEncryptionKeyFromDotenvKey(dotenvKey)
102
+ const key = _parseEncryptionKeyFromDotenvKey(dotenvKey)
102
103
  const decrypted = main.decrypt(ciphertext, key)
103
104
  const raw = fs.readFileSync(filepath, encoding)
104
105
 
105
- return this.hash(decrypted) !== this.hash(raw)
106
+ return hash(decrypted) !== hash(raw)
106
107
  }
107
108
 
108
109
  const hash = function (str) {
@@ -111,7 +112,12 @@ const hash = function (str) {
111
112
 
112
113
  const _parseEncryptionKeyFromDotenvKey = function (dotenvKey) {
113
114
  // Parse DOTENV_KEY. Format is a URI
114
- const uri = new URL(dotenvKey)
115
+ let uri
116
+ try {
117
+ uri = new URL(dotenvKey)
118
+ } catch (e) {
119
+ throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
120
+ }
115
121
 
116
122
  // Get decrypt key
117
123
  const key = uri.password
@@ -122,12 +128,29 @@ const _parseEncryptionKeyFromDotenvKey = function (dotenvKey) {
122
128
  return Buffer.from(key.slice(-64), 'hex')
123
129
  }
124
130
 
125
- const _generateNonce = function () {
126
- return crypto.randomBytes(this._nonceBytes())
127
- }
131
+ const _parseCipherTextFromDotenvKeyAndParsedVault = function (dotenvKey, parsedVault) {
132
+ // Parse DOTENV_KEY. Format is a URI
133
+ let uri
134
+ try {
135
+ uri = new URL(dotenvKey)
136
+ } catch (e) {
137
+ throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
138
+ }
128
139
 
129
- const _nonceBytes = function () {
130
- return 12
140
+ // Get environment
141
+ const environment = uri.searchParams.get('environment')
142
+ if (!environment) {
143
+ throw new Error('INVALID_DOTENV_KEY: Missing environment part')
144
+ }
145
+
146
+ // Get ciphertext payload
147
+ const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
148
+ const ciphertext = parsedVault[environmentKey] // DOTENV_VAULT_PRODUCTION
149
+ if (!ciphertext) {
150
+ throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: cannot locate environment ${environmentKey} in your .env.vault file`)
151
+ }
152
+
153
+ return ciphertext
131
154
  }
132
155
 
133
156
  module.exports = {
@@ -142,6 +165,5 @@ module.exports = {
142
165
  changed,
143
166
  hash,
144
167
  _parseEncryptionKeyFromDotenvKey,
145
- _generateNonce,
146
- _nonceBytes
168
+ _parseCipherTextFromDotenvKeyAndParsedVault
147
169
  }
@@ -0,0 +1,67 @@
1
+ const fs = require('fs')
2
+
3
+ const FORMATS = ['.env*', '!.env.vault', '.flaskenv*']
4
+
5
+ class Generic {
6
+ constructor (filename, touchFile = false) {
7
+ this.filename = filename
8
+ this.formats = FORMATS
9
+ this.touchFile = touchFile
10
+ }
11
+
12
+ append (str) {
13
+ fs.appendFileSync(this.filename, `\n${str}`)
14
+ }
15
+
16
+ run () {
17
+ if (!fs.existsSync(this.filename)) {
18
+ if (this.touchFile === true) {
19
+ fs.writeFileSync(this.filename, '')
20
+ } else {
21
+ return
22
+ }
23
+ }
24
+
25
+ const lines = fs.readFileSync(this.filename, 'utf8').split(/\r?\n/)
26
+ this.formats.forEach(format => {
27
+ if (!lines.includes(format.trim())) {
28
+ this.append(format)
29
+ }
30
+ })
31
+ }
32
+ }
33
+
34
+ class Git {
35
+ run () {
36
+ new Generic('.gitignore', true).run()
37
+ }
38
+ }
39
+
40
+ class Docker {
41
+ run () {
42
+ new Generic('.dockerignore').run()
43
+ }
44
+ }
45
+
46
+ class Npm {
47
+ run () {
48
+ new Generic('.npmignore').run()
49
+ }
50
+ }
51
+
52
+ class Vercel {
53
+ run () {
54
+ new Generic('.vercelignore').run()
55
+ }
56
+ }
57
+
58
+ class AppendToIgnores {
59
+ run () {
60
+ new Docker().run()
61
+ new Git().run()
62
+ new Npm().run()
63
+ new Vercel().run()
64
+ }
65
+ }
66
+
67
+ module.exports = { AppendToIgnores, Generic }
package/src/lib/main.js CHANGED
@@ -9,6 +9,10 @@ const decrypt = function (encrypted, keyStr) {
9
9
  return dotenv.decrypt(encrypted, keyStr)
10
10
  }
11
11
 
12
+ const configDotenv = function (options) {
13
+ return dotenv.configDotenv(options)
14
+ }
15
+
12
16
  const parse = function (src) {
13
17
  const result = dotenv.parse(src)
14
18
 
@@ -57,6 +61,7 @@ const write = function (processEnv = {}, parsed = {}, overload = false) {
57
61
 
58
62
  module.exports = {
59
63
  config,
64
+ configDotenv,
60
65
  decrypt,
61
66
  parse,
62
67
  write