@dotenvx/dotenvx 1.56.0 → 1.57.1

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.
@@ -1,7 +1,50 @@
1
1
  const path = require('path')
2
+ const fs = require('fs')
2
3
  const childProcess = require('child_process')
3
4
  const { logger } = require('../../shared/logger')
4
5
 
6
+ function installCommandForOps () {
7
+ const userAgent = process.env.npm_config_user_agent || ''
8
+ if (userAgent.startsWith('pnpm/')) return 'pnpm add -g @dotenvx/dotenvx-ops'
9
+ if (userAgent.startsWith('yarn/')) return 'yarn global add @dotenvx/dotenvx-ops'
10
+ if (userAgent.startsWith('npm/')) return 'npm i -g @dotenvx/dotenvx-ops'
11
+
12
+ const cwd = process.cwd()
13
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm add -g @dotenvx/dotenvx-ops'
14
+ if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn global add @dotenvx/dotenvx-ops'
15
+ if (
16
+ fs.existsSync(path.join(cwd, 'package-lock.json')) ||
17
+ fs.existsSync(path.join(cwd, 'npm-shrinkwrap.json'))
18
+ ) return 'npm i -g @dotenvx/dotenvx-ops'
19
+
20
+ if (fs.existsSync(path.join(cwd, 'package.json'))) return 'npm i -g @dotenvx/dotenvx-ops'
21
+
22
+ return 'curl -sfS https://dotenvx.sh/ops | sh'
23
+ }
24
+
25
+ function opsBanner (installCommand) {
26
+ const lines = [
27
+ '',
28
+ ' ██████╗ ██████╗ ███████╗',
29
+ ' ██╔═══██╗██╔══██╗██╔════╝',
30
+ ' ██║ ██║██████╔╝███████╗',
31
+ ' ██║ ██║██╔═══╝ ╚════██║',
32
+ ' ╚██████╔╝██║ ███████║',
33
+ ' ╚═════╝ ╚═╝ ╚══════╝',
34
+ '',
35
+ ' KEYS OFF COMPUTER: Add hardened key protection with dotenvx-ops.',
36
+ ` Install now: [${installCommand}]`,
37
+ ' Learn more: [https://dotenvx.com/ops]'
38
+ ]
39
+
40
+ const innerWidth = Math.max(67, ...lines.map((line) => line.length))
41
+ const top = ` ${'_'.repeat(innerWidth)}`
42
+ const middle = lines.map((line) => `|${line.padEnd(innerWidth)}|`).join('\n')
43
+ const bottom = `|${'_'.repeat(innerWidth)}|`
44
+
45
+ return `${top}\n${middle}\n${bottom}`
46
+ }
47
+
5
48
  function executeDynamic (program, command, rawArgs) {
6
49
  if (!command) {
7
50
  program.outputHelp()
@@ -23,26 +66,8 @@ function executeDynamic (program, command, rawArgs) {
23
66
  const result = childProcess.spawnSync(`dotenvx-${command}`, forwardedArgs, { stdio: 'inherit', env })
24
67
  if (result.error) {
25
68
  if (command === 'ops') {
26
- const ops = ` _______________________________________________________________________
27
- | |
28
- | dotenvx-ops: production grade dotenvx–with operational primitives |
29
- | |
30
- | ░▒▓██████▓▒░░▒▓███████▓▒░ ░▒▓███████▓▒░ |
31
- | ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ |
32
- | ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ |
33
- | ░▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░ ░▒▓██████▓▒░ |
34
- | ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ |
35
- | ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ |
36
- | ░▒▓██████▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░ |
37
- | |
38
- | Learn more at https://dotenvx.com/ops |
39
- |_______________________________________________________________________|`
40
-
41
- console.log(ops)
42
- console.log('')
43
- logger.warn(`[INSTALLATION_NEEDED] install dotenvx-${command} to use [dotenvx ${command}] 🛡️`)
44
- logger.help('⮕ next run: [curl -sfS https://dotenvx.sh/ops | sh]')
45
- logger.help('⮕ see more: [https://dotenvx.com/ops]')
69
+ const installCommand = installCommandForOps()
70
+ console.log(opsBanner(installCommand))
46
71
  } else {
47
72
  logger.info(`error: unknown command '${command}'`)
48
73
  }
@@ -1,4 +1,5 @@
1
1
  const fsx = require('./fsx')
2
+ const Errors = require('./errors')
2
3
 
3
4
  const RESERVED_ENV_FILES = ['.env.project', '.env.keys', '.env.me', '.env.x', '.env.example']
4
5
 
@@ -14,10 +15,7 @@ function findEnvFiles (directory) {
14
15
  return envFiles
15
16
  } catch (e) {
16
17
  if (e.code === 'ENOENT') {
17
- const error = new Error(`missing directory (${directory})`)
18
- error.code = 'MISSING_DIRECTORY'
19
-
20
- throw error
18
+ throw new Errors({ directory }).missingDirectory()
21
19
  } else {
22
20
  throw e
23
21
  }
@@ -1,5 +1,6 @@
1
1
  const fsx = require('./fsx')
2
2
  const path = require('path')
3
+ const Errors = require('./errors')
3
4
 
4
5
  const HOOK_SCRIPT = `#!/bin/sh
5
6
 
@@ -45,8 +46,7 @@ class InstallPrecommitHook {
45
46
  successMessage
46
47
  }
47
48
  } catch (err) {
48
- const error = new Error(`failed to modify pre-commit hook: ${err.message}`)
49
- throw error
49
+ throw new Errors({ error: err }).precommitHookModifyFailed()
50
50
  }
51
51
  }
52
52
 
@@ -0,0 +1,11 @@
1
+ const path = require('path')
2
+
3
+ function localDisplayPath (filepath) {
4
+ if (!filepath) return '.env.keys'
5
+ if (!path.isAbsolute(filepath)) return filepath
6
+
7
+ const relative = path.relative(process.cwd(), filepath)
8
+ return relative || path.basename(filepath)
9
+ }
10
+
11
+ module.exports = localDisplayPath
package/src/lib/main.js CHANGED
@@ -17,7 +17,7 @@ const Genexample = require('./services/genexample')
17
17
  const buildEnvs = require('./helpers/buildEnvs')
18
18
  const Parse = require('./helpers/parse')
19
19
  const fsx = require('./helpers/fsx')
20
- const isIgnoringDotenvKeys = require('./helpers/isIgnoringDotenvKeys')
20
+ const localDisplayPath = require('./helpers/localDisplayPath')
21
21
 
22
22
  /** @type {import('./main').config} */
23
23
  const config = function (options = {}) {
@@ -81,16 +81,10 @@ const config = function (options = {}) {
81
81
 
82
82
  if (error.code === 'MISSING_ENV_FILE') {
83
83
  if (!options.convention) { // do not output error for conventions (too noisy)
84
- logger.error(error.message)
85
- if (error.help) {
86
- logger.error(error.help)
87
- }
84
+ logger.error(error.messageWithHelp)
88
85
  }
89
86
  } else {
90
- logger.error(error.message)
91
- if (error.help) {
92
- logger.error(error.help)
93
- }
87
+ logger.error(error.messageWithHelp)
94
88
  }
95
89
  }
96
90
 
@@ -127,10 +121,7 @@ const config = function (options = {}) {
127
121
  } catch (error) {
128
122
  if (strict) throw error // throw immediately if strict
129
123
 
130
- logger.error(error.message)
131
- if (error.help) {
132
- logger.help(error.help)
133
- }
124
+ logger.error(error.messageWithHelp)
134
125
 
135
126
  return { parsed: {}, error }
136
127
  }
@@ -154,10 +145,7 @@ const parse = function (src, options = {}) {
154
145
 
155
146
  // display any errors
156
147
  for (const error of errors) {
157
- logger.error(error.message)
158
- if (error.help) {
159
- logger.error(error.help)
160
- }
148
+ logger.error(error.messageWithHelp)
161
149
  }
162
150
 
163
151
  return parsed
@@ -199,15 +187,9 @@ const set = function (key, value, options = {}) {
199
187
  logger.verbose(`setting for ${processedEnv.envFilepath}`)
200
188
 
201
189
  if (processedEnv.error) {
202
- if (processedEnv.error.code === 'MISSING_ENV_FILE') {
203
- logger.warn(processedEnv.error.message)
204
- logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx set]`)
205
- } else {
206
- logger.warn(processedEnv.error.message)
207
- if (processedEnv.error.help) {
208
- logger.help(processedEnv.error.help)
209
- }
210
- }
190
+ const error = processedEnv.error
191
+ const message = error.messageWithHelp || (error.help ? `${error.message}. ${error.help}` : error.message)
192
+ logger.warn(message)
211
193
  } else {
212
194
  fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc)
213
195
 
@@ -216,26 +198,25 @@ const set = function (key, value, options = {}) {
216
198
  }
217
199
  }
218
200
 
201
+ const keyAddedEnv = processedEnvs.find((processedEnv) => processedEnv.privateKeyAdded)
202
+ const keyAddedSuffix = keyAddedEnv ? ` + key (${localDisplayPath(keyAddedEnv.envKeysFilepath)})` : ''
203
+
219
204
  if (changedFilepaths.length > 0) {
220
- logger.success(`✔ set ${key}${withEncryption} (${changedFilepaths.join(',')})`)
205
+ if (encrypt) {
206
+ logger.success(`◈ encrypted ${key} (${changedFilepaths.join(',')})${keyAddedSuffix}`)
207
+ } else {
208
+ logger.success(`◇ set ${key} (${changedFilepaths.join(',')})`)
209
+ }
210
+ } else if (encrypt && keyAddedEnv) {
211
+ const keyAddedEnvFilepath = keyAddedEnv.envFilepath || changedFilepaths[0] || '.env'
212
+ logger.success(`◈ encrypted ${key} (${keyAddedEnvFilepath})${keyAddedSuffix}`)
221
213
  } else if (unchangedFilepaths.length > 0) {
222
- logger.info(`no changes (${unchangedFilepaths})`)
214
+ logger.info(`○ no changes (${unchangedFilepaths})`)
223
215
  } else {
224
216
  // do nothing
225
217
  }
226
218
 
227
- for (const processedEnv of processedEnvs) {
228
- if (processedEnv.privateKeyAdded) {
229
- logger.success(`✔ key added to ${processedEnv.envKeysFilepath} (${processedEnv.privateKeyName})`)
230
- // logger.help('⮕ optional: [dotenvx ops backup] to securely backup private key')
231
-
232
- if (!isIgnoringDotenvKeys()) {
233
- logger.help('⮕ next run: [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys')
234
- }
235
-
236
- logger.help(`⮕ next run: [${processedEnv.privateKeyName}='${processedEnv.privateKey}' dotenvx get ${key}] to test decryption locally`)
237
- }
238
- }
219
+ // intentionally quiet: success line communicates key creation
239
220
 
240
221
  return {
241
222
  processedEnvs,
@@ -261,10 +242,7 @@ const get = function (key, options = {}) {
261
242
 
262
243
  if (options.strict) throw error // throw immediately if strict
263
244
 
264
- logger.error(error.message)
265
- if (error.help) {
266
- logger.error(error.help)
267
- }
245
+ logger.error(error.messageWithHelp)
268
246
  }
269
247
 
270
248
  if (key) {
@@ -122,7 +122,12 @@ class Encrypt {
122
122
  if (!encrypted) {
123
123
  row.keys.push(key) // track key(s)
124
124
 
125
- const encryptedValue = encryptValue(value, publicKey)
125
+ let encryptedValue
126
+ try {
127
+ encryptedValue = encryptValue(value, publicKey)
128
+ } catch {
129
+ throw new Errors({ publicKeyName, publicKey }).invalidPublicKey()
130
+ }
126
131
 
127
132
  // once newSrc is built write it out
128
133
  envSrc = replace(envSrc, key, encryptedValue)
@@ -17,14 +17,7 @@ class Genexample {
17
17
 
18
18
  run () {
19
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
20
+ throw new Errors().missingEnvFiles()
28
21
  }
29
22
 
30
23
  const keys = new Set()
@@ -4,6 +4,7 @@ const path = require('path')
4
4
  const ignore = require('ignore')
5
5
 
6
6
  const Ls = require('../services/ls')
7
+ const Errors = require('../helpers/errors')
7
8
 
8
9
  const isFullyEncrypted = require('./../helpers/isFullyEncrypted')
9
10
  const packageJson = require('./../helpers/packageJson')
@@ -24,7 +25,10 @@ class Prebuild {
24
25
 
25
26
  // 1. check for .dockerignore file
26
27
  if (!fsx.existsSync('.dockerignore')) {
27
- const warning = new Error(`[dotenvx@${packageJson.version}][prebuild] .dockerignore missing`)
28
+ const warning = new Errors({
29
+ message: `[dotenvx@${packageJson.version}][prebuild] .dockerignore missing`,
30
+ help: 'fix: [touch .dockerignore]'
31
+ }).custom()
28
32
  warnings.push(warning)
29
33
  } else {
30
34
  dockerignore = fsx.readFileX('.dockerignore')
@@ -42,8 +46,10 @@ class Prebuild {
42
46
  // check if that file is being ignored
43
47
  if (ig.ignores(file)) {
44
48
  if (file === '.env.example' || file === '.env.x') {
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}]`
49
+ const warning = new Errors({
50
+ message: `[dotenvx@${packageJson.version}][prebuild] ${file} (currently ignored but should not be)`,
51
+ help: `fix: [dotenvx ext gitignore --pattern !${file}]`
52
+ }).custom()
47
53
  warnings.push(warning)
48
54
  }
49
55
  } else {
@@ -54,15 +60,13 @@ class Prebuild {
54
60
  // if contents are encrypted don't raise an error
55
61
  if (!encrypted) {
56
62
  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}]`
63
+ let errorHelp = `fix: [dotenvx encrypt -f ${file}] or [dotenvx ext gitignore --pattern ${file}]`
58
64
  if (file.includes('.env.keys')) {
59
65
  errorMsg = `[dotenvx@${packageJson.version}][prebuild] ${file} not protected (dockerignored)`
60
- errorHelp = `[dotenvx@${packageJson.version}][prebuild] ⮕ run [dotenvx ext gitignore --pattern ${file}]`
66
+ errorHelp = `fix: [dotenvx ext gitignore --pattern ${file}]`
61
67
  }
62
68
 
63
- const error = new Error(errorMsg)
64
- error.help = errorHelp
65
- throw error
69
+ throw new Errors({ message: errorMsg, help: errorHelp }).custom()
66
70
  }
67
71
  }
68
72
  }
@@ -8,6 +8,7 @@ const Ls = require('../services/ls')
8
8
  const isFullyEncrypted = require('./../helpers/isFullyEncrypted')
9
9
  const packageJson = require('./../helpers/packageJson')
10
10
  const InstallPrecommitHook = require('./../helpers/installPrecommitHook')
11
+ const Errors = require('./../helpers/errors')
11
12
  const childProcess = require('child_process')
12
13
  const MISSING_GITIGNORE = '.env.keys' // by default only ignore .env.keys. all other .env* files COULD be included - as long as they are encrypted
13
14
 
@@ -37,7 +38,10 @@ class Precommit {
37
38
 
38
39
  // 1. check for .gitignore file
39
40
  if (!fsx.existsSync('.gitignore')) {
40
- const warning = new Error(`[dotenvx@${packageJson.version}][precommit] .gitignore missing`)
41
+ const warning = new Errors({
42
+ message: `[dotenvx@${packageJson.version}][precommit] .gitignore missing`,
43
+ help: 'fix: [touch .gitignore]'
44
+ }).custom()
41
45
  warnings.push(warning)
42
46
  } else {
43
47
  gitignore = fsx.readFileX('.gitignore')
@@ -58,8 +62,10 @@ class Precommit {
58
62
  // check if that file is being ignored
59
63
  if (ig.ignores(file)) {
60
64
  if (file === '.env.example' || file === '.env.x') {
61
- const warning = new Error(`[dotenvx@${packageJson.version}][precommit] ${file} (currently ignored but should not be)`)
62
- warning.help = `[dotenvx@${packageJson.version}][precommit] ⮕ run [dotenvx ext gitignore --pattern !${file}]`
65
+ const warning = new Errors({
66
+ message: `[dotenvx@${packageJson.version}][precommit] ${file} (currently ignored but should not be)`,
67
+ help: `fix: [dotenvx ext gitignore --pattern !${file}]`
68
+ }).custom()
63
69
  warnings.push(warning)
64
70
  }
65
71
  } else {
@@ -70,15 +76,13 @@ class Precommit {
70
76
  // if contents are encrypted don't raise an error
71
77
  if (!encrypted) {
72
78
  let errorMsg = `[dotenvx@${packageJson.version}][precommit] ${file} not protected (encrypted or gitignored)`
73
- let errorHelp = `[dotenvx@${packageJson.version}][precommit] ⮕ run [dotenvx encrypt -f ${file}] or [dotenvx ext gitignore --pattern ${file}]`
79
+ let errorHelp = `fix: [dotenvx encrypt -f ${file}] or [dotenvx ext gitignore --pattern ${file}]`
74
80
  if (file.includes('.env.keys')) {
75
81
  errorMsg = `[dotenvx@${packageJson.version}][precommit] ${file} not protected (gitignored)`
76
- errorHelp = `[dotenvx@${packageJson.version}][precommit] ⮕ run [dotenvx ext gitignore --pattern ${file}]`
82
+ errorHelp = `fix: [dotenvx ext gitignore --pattern ${file}]`
77
83
  }
78
84
 
79
- const error = new Error(errorMsg)
80
- error.help = errorHelp
81
- throw error
85
+ throw new Errors({ message: errorMsg, help: errorHelp }).custom()
82
86
  }
83
87
  }
84
88
  }
@@ -131,7 +131,12 @@ class Rotate {
131
131
  row.keys.push(key) // track key(s)
132
132
 
133
133
  const decryptedValue = decryptKeyValue(key, value, privateKeyName, privateKeyValue) // get decrypted value
134
- const encryptedValue = encryptValue(decryptedValue, newPublicKey) // encrypt with the new publicKey
134
+ let encryptedValue
135
+ try {
136
+ encryptedValue = encryptValue(decryptedValue, newPublicKey) // encrypt with the new publicKey
137
+ } catch {
138
+ throw new Errors({ publicKeyName, publicKey: newPublicKey }).invalidPublicKey()
139
+ }
135
140
 
136
141
  envSrc = replace(envSrc, key, encryptedValue)
137
142
  }
@@ -109,7 +109,11 @@ class Sets {
109
109
 
110
110
  row.publicKey = publicKey
111
111
  row.privateKey = privateKey
112
- row.encryptedValue = encryptValue(this.value, publicKey)
112
+ try {
113
+ row.encryptedValue = encryptValue(this.value, publicKey)
114
+ } catch {
115
+ throw new Errors({ publicKeyName, publicKey }).invalidPublicKey()
116
+ }
113
117
  row.privateKeyName = privateKeyName
114
118
  }
115
119
 
@@ -1,11 +1,13 @@
1
1
  const depth = require('../lib/helpers/colorDepth')
2
+ const Errors = require('../lib/helpers/errors')
2
3
 
3
4
  const colors16 = new Map([
5
+ ['amber', 33],
4
6
  ['blue', 34],
5
7
  ['gray', 37],
6
8
  ['green', 32],
7
9
  ['olive', 33],
8
- ['orangered', 31], // mapped to red
10
+ ['orangered', 33], // mapped to yellow/brown
9
11
  ['plum', 35], // mapped to magenta
10
12
  ['red', 31],
11
13
  ['electricblue', 36],
@@ -13,21 +15,32 @@ const colors16 = new Map([
13
15
  ])
14
16
 
15
17
  const colors256 = new Map([
18
+ ['amber', 136],
16
19
  ['blue', 21],
17
20
  ['gray', 244],
18
21
  ['green', 34],
19
22
  ['olive', 142],
20
- ['orangered', 202],
23
+ ['orangered', 130], // burnished copper
21
24
  ['plum', 182],
22
- ['red', 196],
25
+ ['red', 124], // brighter garnet
23
26
  ['electricblue', 45],
24
27
  ['dodgerblue', 33]
25
28
  ])
26
29
 
30
+ const colorsTrueColor = new Map([
31
+ ['amber', [236, 213, 63]],
32
+ ['orangered', [138, 90, 43]], // #8A5A2B burnished copper
33
+ ['red', [140, 35, 50]] // #8C2332 brighter garnet
34
+ ])
35
+
27
36
  function getColor (color) {
28
37
  const colorDepth = depth.getColorDepth()
29
38
  if (!colors256.has(color)) {
30
- throw new Error(`Invalid color ${color}`)
39
+ throw new Errors({ color }).invalidColor()
40
+ }
41
+ if (colorDepth >= 24 && colorsTrueColor.has(color)) {
42
+ const [r, g, b] = colorsTrueColor.get(color)
43
+ return (message) => `\x1b[38;2;${r};${g};${b}m${message}\x1b[39m`
31
44
  }
32
45
  if (colorDepth >= 8) {
33
46
  const code = colors256.get(color)
@@ -1,4 +1,5 @@
1
1
  const packageJson = require('../lib/helpers/packageJson')
2
+ const Errors = require('../lib/helpers/errors')
2
3
  const { getColor, bold } = require('./colors')
3
4
 
4
5
  const levels = {
@@ -13,10 +14,11 @@ const levels = {
13
14
  silly: 6
14
15
  }
15
16
 
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
17
+ const error = (m) => bold(getColor('red')(`☠ ${m}`))
18
+ const warn = (m) => getColor('orangered')(`⚠ ${m}`)
19
+ const success = getColor('amber')
20
+ const successv = getColor('amber')
21
+ const info = getColor('gray')
20
22
  const help = getColor('dodgerblue')
21
23
  const verbose = getColor('plum')
22
24
  const debug = getColor('plum')
@@ -32,7 +34,7 @@ function stderr (level, message) {
32
34
 
33
35
  function stdout (level, message) {
34
36
  if (levels[level] === undefined) {
35
- throw new Error(`MISSING_LOG_LEVEL: '${level}'. implement in logger.`)
37
+ throw new Errors({ level }).missingLogLevel()
36
38
  }
37
39
 
38
40
  if (levels[level] <= currentLevel) {
@@ -58,7 +60,7 @@ function formatMessage (level, message) {
58
60
  return successv(`[${currentName}@${currentVersion}] ${formattedMessage}`)
59
61
  // info
60
62
  case 'info':
61
- return formattedMessage
63
+ return info(formattedMessage)
62
64
  // help
63
65
  case 'help':
64
66
  return help(formattedMessage)