@dotenvx/dotenvx 0.16.1 → 0.17.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/package.json +3 -3
- package/src/cli/actions/encrypt.js +49 -121
- package/src/cli/dotenvx.js +2 -2
- package/src/lib/helpers/dotenvKeys.js +68 -0
- package/src/lib/helpers/dotenvVault.js +151 -0
- package/src/lib/main.js +7 -1
- package/src/lib/services/encrypt.js +115 -0
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.17.0",
|
|
3
3
|
"name": "@dotenvx/dotenvx",
|
|
4
4
|
"description": "a better dotenv–from the creator of `dotenv`",
|
|
5
5
|
"author": "@motdotla",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"prerelease": "npm test",
|
|
26
26
|
"release": "standard-version"
|
|
27
27
|
},
|
|
28
|
-
"funding": "
|
|
28
|
+
"funding": "https://dotenvx.com",
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@inquirer/prompts": "^3.3.0",
|
|
31
31
|
"chalk": "^4.1.2",
|
|
32
32
|
"clipboardy": "^2.3.0",
|
|
33
33
|
"commander": "^11.1.0",
|
|
34
34
|
"conf": "^10.2.0",
|
|
35
|
-
"dotenv": "
|
|
35
|
+
"dotenv": "16.4.5",
|
|
36
36
|
"dotenv-expand": "^11.0.6",
|
|
37
37
|
"execa": "^5.1.1",
|
|
38
38
|
"glob": "^10.3.10",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
2
3
|
|
|
3
4
|
const main = require('./../../lib/main')
|
|
4
5
|
const logger = require('./../../shared/logger')
|
|
@@ -7,149 +8,76 @@ const createSpinner = require('./../../shared/createSpinner')
|
|
|
7
8
|
|
|
8
9
|
const spinner = createSpinner('encrypting')
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
const ENCODING = 'utf8'
|
|
12
|
-
|
|
13
|
-
async function encrypt () {
|
|
11
|
+
async function encrypt (directory) {
|
|
14
12
|
spinner.start()
|
|
15
13
|
await helpers.sleep(500) // better dx
|
|
16
14
|
|
|
15
|
+
logger.debug(`directory: ${directory}`)
|
|
16
|
+
|
|
17
17
|
const options = this.opts()
|
|
18
18
|
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
if (!Array.isArray(optionEnvFile)) {
|
|
22
|
-
optionEnvFile = [optionEnvFile]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const addedKeys = new Set()
|
|
26
|
-
const addedVaults = new Set()
|
|
27
|
-
const addedEnvFilepaths = new Set()
|
|
28
|
-
|
|
29
|
-
// must be at least one .env* file
|
|
30
|
-
if (optionEnvFile.length < 1) {
|
|
31
|
-
spinner.fail('no .env* files found')
|
|
32
|
-
logger.help('? add one with [echo "HELLO=World" > .env] and then run [dotenvx encrypt]')
|
|
33
|
-
process.exit(1)
|
|
34
|
-
}
|
|
20
|
+
const optionEnvFile = options.envFile || helpers.findEnvFiles(directory)
|
|
35
21
|
|
|
36
22
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const environment = helpers.guessEnvironment(filepath)
|
|
50
|
-
const key = `DOTENV_KEY_${environment.toUpperCase()}`
|
|
23
|
+
const {
|
|
24
|
+
dotenvKeys,
|
|
25
|
+
dotenvKeysFile,
|
|
26
|
+
addedKeys,
|
|
27
|
+
existingKeys,
|
|
28
|
+
dotenvVaultFile,
|
|
29
|
+
addedVaults,
|
|
30
|
+
existingVaults,
|
|
31
|
+
addedDotenvFilenames
|
|
32
|
+
} = main.encrypt(directory, optionEnvFile)
|
|
51
33
|
|
|
52
|
-
|
|
34
|
+
logger.verbose(`generating .env.keys from ${optionEnvFile}`)
|
|
35
|
+
if (addedKeys.length > 0) {
|
|
36
|
+
logger.verbose(`generated ${addedKeys}`)
|
|
37
|
+
}
|
|
38
|
+
if (existingKeys.length > 0) {
|
|
39
|
+
logger.verbose(`existing ${existingKeys}`)
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(path.resolve(directory, '.env.keys'), dotenvKeysFile)
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
43
|
+
logger.verbose(`generating .env.vault from ${optionEnvFile}`)
|
|
44
|
+
if (addedVaults.length > 0) {
|
|
45
|
+
logger.verbose(`encrypting ${addedVaults}`)
|
|
46
|
+
}
|
|
47
|
+
if (existingVaults.length > 0) {
|
|
48
|
+
logger.verbose(`existing ${existingVaults}`)
|
|
49
|
+
}
|
|
50
|
+
fs.writeFileSync(path.resolve(directory, '.env.vault'), dotenvVaultFile)
|
|
59
51
|
|
|
60
|
-
|
|
52
|
+
if (addedDotenvFilenames.length > 0) {
|
|
53
|
+
spinner.succeed(`encrypted to .env.vault (${addedDotenvFilenames})`)
|
|
54
|
+
logger.help2('ℹ commit .env.vault to code: [git commit -am ".env.vault"]')
|
|
55
|
+
} else {
|
|
56
|
+
spinner.done(`no changes (${optionEnvFile})`)
|
|
57
|
+
}
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
logger.debug(`existing ${key} as ${value}`)
|
|
66
|
-
}
|
|
59
|
+
if (addedKeys.length > 0) {
|
|
60
|
+
spinner.succeed(`${helpers.pluralize('key', addedKeys.length)} added to .env.keys (${addedKeys})`)
|
|
61
|
+
logger.help2('ℹ push .env.keys up to hub: [dotenvx hub push]')
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
if (addedVaults.length > 0) {
|
|
65
|
+
const DOTENV_VAULT_X = addedVaults[addedVaults.length - 1]
|
|
66
|
+
const DOTENV_KEY_X = DOTENV_VAULT_X.replace('_VAULT_', '_KEY_')
|
|
67
|
+
const tryKey = dotenvKeys[DOTENV_KEY_X] || '<dotenv_key_environment>'
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
const value = dotenvKeys[key]
|
|
76
|
-
keysData += `${key}="${value}"\n`
|
|
69
|
+
logger.help2(`ℹ run [DOTENV_KEY='${tryKey}' dotenvx run -- yourcommand] to test decryption locally`)
|
|
77
70
|
}
|
|
78
|
-
|
|
79
|
-
fs.writeFileSync('.env.keys', keysData)
|
|
80
71
|
} catch (error) {
|
|
81
72
|
spinner.fail(error.message)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// used later in logging to user
|
|
86
|
-
const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
logger.verbose(`generating .env.vault from ${optionEnvFile}`)
|
|
90
|
-
|
|
91
|
-
const dotenvVaults = (main.configDotenv({ path: '.env.vault' }).parsed || {})
|
|
92
|
-
|
|
93
|
-
for (const envFilepath of optionEnvFile) {
|
|
94
|
-
const filepath = helpers.resolvePath(envFilepath)
|
|
95
|
-
const environment = helpers.guessEnvironment(filepath)
|
|
96
|
-
const vault = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
97
|
-
|
|
98
|
-
let ciphertext = dotenvVaults[vault]
|
|
99
|
-
const dotenvKey = dotenvKeys[`DOTENV_KEY_${environment.toUpperCase()}`]
|
|
100
|
-
|
|
101
|
-
if (!ciphertext || ciphertext.length === 0 || helpers.changed(ciphertext, dotenvKey, filepath, ENCODING)) {
|
|
102
|
-
logger.verbose(`encrypting ${vault}`)
|
|
103
|
-
ciphertext = helpers.encryptFile(filepath, dotenvKey, ENCODING)
|
|
104
|
-
logger.verbose(`encrypting ${vault} as ${ciphertext}`)
|
|
105
|
-
|
|
106
|
-
dotenvVaults[vault] = ciphertext
|
|
107
|
-
|
|
108
|
-
addedVaults.add(vault) // for info logging to user
|
|
109
|
-
addedEnvFilepaths.add(envFilepath) // for info logging to user
|
|
110
|
-
} else {
|
|
111
|
-
logger.verbose(`existing ${vault}`)
|
|
112
|
-
logger.debug(`existing ${vault} as ${ciphertext}`)
|
|
113
|
-
}
|
|
73
|
+
if (error.help) {
|
|
74
|
+
logger.help(error.help)
|
|
114
75
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
#/ cloud-agnostic vaulting standard /
|
|
118
|
-
#/ [how it works](https://dotenvx.com/env-vault) /
|
|
119
|
-
#/--------------------------------------------------/\n\n`
|
|
120
|
-
|
|
121
|
-
for (const vault in dotenvVaults) {
|
|
122
|
-
const value = dotenvVaults[vault]
|
|
123
|
-
const environment = vault.replace('DOTENV_VAULT_', '').toLowerCase()
|
|
124
|
-
vaultData += `# ${environment}\n`
|
|
125
|
-
vaultData += `${vault}="${value}"\n\n`
|
|
76
|
+
if (error.code) {
|
|
77
|
+
logger.debug(`ERROR_CODE: ${error.code}`)
|
|
126
78
|
}
|
|
127
|
-
|
|
128
|
-
fs.writeFileSync('.env.vault', vaultData)
|
|
129
|
-
} catch (e) {
|
|
130
|
-
spinner.fail(e.message)
|
|
131
79
|
process.exit(1)
|
|
132
80
|
}
|
|
133
|
-
|
|
134
|
-
if (addedEnvFilepaths.size > 0) {
|
|
135
|
-
spinner.succeed(`encrypted to .env.vault (${[...addedEnvFilepaths]})`)
|
|
136
|
-
logger.help2('ℹ commit .env.vault to code: [git commit -am ".env.vault"]')
|
|
137
|
-
} else {
|
|
138
|
-
spinner.done(`no changes (${optionEnvFile})`)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (addedKeys.size > 0) {
|
|
142
|
-
spinner.succeed(`${helpers.pluralize('key', addedKeys.size)} added to .env.keys (${[...addedKeys]})`)
|
|
143
|
-
logger.help2('ℹ push .env.keys up to hub: [dotenvx hub push]')
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (addedVaults.size > 0) {
|
|
147
|
-
const DOTENV_VAULT_X = [...addedVaults][addedVaults.size - 1]
|
|
148
|
-
const DOTENV_KEY_X = DOTENV_VAULT_X.replace('_VAULT_', '_KEY_')
|
|
149
|
-
const tryKey = dotenvKeys[DOTENV_KEY_X] || '<dotenv_key_environment>'
|
|
150
|
-
|
|
151
|
-
logger.help2(`ℹ run [DOTENV_KEY='${tryKey}' dotenvx run -- yourcommand] to test decryption locally`)
|
|
152
|
-
}
|
|
153
81
|
}
|
|
154
82
|
|
|
155
83
|
module.exports = encrypt
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -5,7 +5,6 @@ const { Command } = require('commander')
|
|
|
5
5
|
const program = new Command()
|
|
6
6
|
|
|
7
7
|
const logger = require('./../shared/logger')
|
|
8
|
-
const helpers = require('./helpers')
|
|
9
8
|
const examples = require('./examples')
|
|
10
9
|
const packageJson = require('./../shared/packageJson')
|
|
11
10
|
|
|
@@ -64,7 +63,8 @@ program.command('run')
|
|
|
64
63
|
program.command('encrypt')
|
|
65
64
|
.description('encrypt .env.* to .env.vault')
|
|
66
65
|
.addHelpText('after', examples.encrypt)
|
|
67
|
-
.
|
|
66
|
+
.argument('[directory]', 'directory to encrypt', '.')
|
|
67
|
+
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)')
|
|
68
68
|
.action(require('./actions/encrypt'))
|
|
69
69
|
|
|
70
70
|
// dotenvx decrypt
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const crypto = require('crypto')
|
|
3
|
+
|
|
4
|
+
class DotenvKeys {
|
|
5
|
+
constructor (envFilepaths = [], dotenvKeys = {}) {
|
|
6
|
+
this.envFilepaths = envFilepaths // pass .env* filepaths to be encrypted
|
|
7
|
+
this.dotenvKeys = dotenvKeys // pass current parsed dotenv keys from .env.keys file
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
run () {
|
|
11
|
+
const addedKeys = new Set()
|
|
12
|
+
const existingKeys = new Set()
|
|
13
|
+
|
|
14
|
+
for (const filepath of this.envFilepaths) {
|
|
15
|
+
const environment = this._guessEnvironment(filepath)
|
|
16
|
+
const key = `DOTENV_KEY_${environment.toUpperCase()}`
|
|
17
|
+
|
|
18
|
+
let value = this.dotenvKeys[key]
|
|
19
|
+
|
|
20
|
+
// first time seeing new DOTENV_KEY_${environment}
|
|
21
|
+
if (!value || value.length === 0) {
|
|
22
|
+
value = this._generateDotenvKey(environment)
|
|
23
|
+
|
|
24
|
+
this.dotenvKeys[key] = value
|
|
25
|
+
|
|
26
|
+
addedKeys.add(key) // for info logging to user
|
|
27
|
+
} else {
|
|
28
|
+
existingKeys.add(key) // for info logging to user
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let keysData = `#/!!!!!!!!!!!!!!!!!!!.env.keys!!!!!!!!!!!!!!!!!!!!!!/
|
|
33
|
+
#/ DOTENV_KEYs. DO NOT commit to source control /
|
|
34
|
+
#/ [how it works](https://dotenvx.com/env-keys) /
|
|
35
|
+
#/--------------------------------------------------/\n`
|
|
36
|
+
|
|
37
|
+
for (const [key, value] of Object.entries(this.dotenvKeys)) {
|
|
38
|
+
keysData += `${key}="${value}"\n`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
dotenvKeys: this.dotenvKeys,
|
|
43
|
+
dotenvKeysFile: keysData,
|
|
44
|
+
addedKeys: [...addedKeys], // return set as array
|
|
45
|
+
existingKeys: [...existingKeys] // return set as array
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_guessEnvironment (filepath) {
|
|
50
|
+
const filename = path.basename(filepath)
|
|
51
|
+
const parts = filename.split('.')
|
|
52
|
+
const possibleEnvironment = parts[2] // ['', 'env', environment', 'previous']
|
|
53
|
+
|
|
54
|
+
if (!possibleEnvironment || possibleEnvironment.length === 0) {
|
|
55
|
+
return 'development'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return possibleEnvironment
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_generateDotenvKey (environment) {
|
|
62
|
+
const rand = crypto.randomBytes(32).toString('hex')
|
|
63
|
+
|
|
64
|
+
return `dotenv://:key_${rand}@dotenvx.com/vault/.env.vault?environment=${environment.toLowerCase()}`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = DotenvKeys
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const crypto = require('crypto')
|
|
3
|
+
const xxhash = require('xxhashjs')
|
|
4
|
+
const dotenv = require('dotenv')
|
|
5
|
+
|
|
6
|
+
const XXHASH_SEED = 0xABCD
|
|
7
|
+
const NONCE_BYTES = 12
|
|
8
|
+
|
|
9
|
+
class DotenvVault {
|
|
10
|
+
constructor (dotenvFiles = {}, dotenvKeys = {}, dotenvVaults = {}) {
|
|
11
|
+
this.dotenvFiles = dotenvFiles // key: filepath and value: filecontent
|
|
12
|
+
this.dotenvKeys = dotenvKeys // pass current parsed dotenv keys from .env.keys
|
|
13
|
+
this.dotenvVaults = dotenvVaults // pass current parsed dotenv vaults from .env.vault
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
run () {
|
|
17
|
+
const addedVaults = new Set()
|
|
18
|
+
const existingVaults = new Set()
|
|
19
|
+
const addedDotenvFilenames = new Set()
|
|
20
|
+
|
|
21
|
+
for (const [filepath, raw] of Object.entries(this.dotenvFiles)) {
|
|
22
|
+
const environment = this._guessEnvironment(filepath)
|
|
23
|
+
const vault = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
24
|
+
|
|
25
|
+
let ciphertext = this.dotenvVaults[vault]
|
|
26
|
+
const dotenvKey = this.dotenvKeys[`DOTENV_KEY_${environment.toUpperCase()}`]
|
|
27
|
+
|
|
28
|
+
if (!ciphertext || ciphertext.length === 0 || this._changed(dotenvKey, ciphertext, raw)) {
|
|
29
|
+
ciphertext = this._encrypt(dotenvKey, raw)
|
|
30
|
+
this.dotenvVaults[vault] = ciphertext
|
|
31
|
+
addedVaults.add(vault) // for info logging to user
|
|
32
|
+
|
|
33
|
+
addedDotenvFilenames.add(path.basename(filepath)) // for info logging to user
|
|
34
|
+
} else {
|
|
35
|
+
existingVaults.add(vault) // for info logging to user
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let vaultData = `#/-------------------.env.vault---------------------/
|
|
40
|
+
#/ cloud-agnostic vaulting standard /
|
|
41
|
+
#/ [how it works](https://dotenvx.com/env-vault) /
|
|
42
|
+
#/--------------------------------------------------/\n\n`
|
|
43
|
+
|
|
44
|
+
for (const [vault, value] of Object.entries(this.dotenvVaults)) {
|
|
45
|
+
const environment = vault.replace('DOTENV_VAULT_', '').toLowerCase()
|
|
46
|
+
vaultData += `# ${environment}\n`
|
|
47
|
+
vaultData += `${vault}="${value}"\n\n`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
dotenvVaultFile: vaultData,
|
|
52
|
+
addedVaults: [...addedVaults], // return set as array
|
|
53
|
+
existingVaults: [...existingVaults], // return set as array
|
|
54
|
+
addedDotenvFilenames: [...addedDotenvFilenames] // return set as array
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_guessEnvironment (filepath) {
|
|
59
|
+
const filename = path.basename(filepath)
|
|
60
|
+
const parts = filename.split('.')
|
|
61
|
+
const possibleEnvironment = parts[2] // ['', 'env', environment', 'previous']
|
|
62
|
+
|
|
63
|
+
if (!possibleEnvironment || possibleEnvironment.length === 0) {
|
|
64
|
+
return 'development'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return possibleEnvironment
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_changed (dotenvKey, ciphertext, raw) {
|
|
71
|
+
const decrypted = this._decrypt(dotenvKey, ciphertext)
|
|
72
|
+
|
|
73
|
+
return this._hash(decrypted) !== this._hash(raw)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_encrypt (dotenvKey, raw) {
|
|
77
|
+
const key = this._parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
78
|
+
|
|
79
|
+
// set up nonce
|
|
80
|
+
const nonce = crypto.randomBytes(NONCE_BYTES)
|
|
81
|
+
|
|
82
|
+
// set up cipher
|
|
83
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
|
|
84
|
+
|
|
85
|
+
// generate ciphertext
|
|
86
|
+
let ciphertext = ''
|
|
87
|
+
ciphertext += cipher.update(raw, 'utf8', 'hex')
|
|
88
|
+
ciphertext += cipher.final('hex')
|
|
89
|
+
ciphertext += cipher.getAuthTag().toString('hex')
|
|
90
|
+
|
|
91
|
+
// prepend nonce
|
|
92
|
+
ciphertext = nonce.toString('hex') + ciphertext
|
|
93
|
+
|
|
94
|
+
// base64 encode output
|
|
95
|
+
return Buffer.from(ciphertext, 'hex').toString('base64')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_decrypt (dotenvKey, ciphertext) {
|
|
99
|
+
const key = this._parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
return dotenv.decrypt(ciphertext, key)
|
|
103
|
+
} catch (e) {
|
|
104
|
+
const decryptionFailedError = new Error('[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.')
|
|
105
|
+
decryptionFailedError.code = 'DECRYPTION_FAILED'
|
|
106
|
+
decryptionFailedError.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.'
|
|
107
|
+
decryptionFailedError.debug = `[DECRYPTION_FAILED] DOTENV_KEY is ${dotenvKey}`
|
|
108
|
+
|
|
109
|
+
switch (e.code) {
|
|
110
|
+
case 'DECRYPTION_FAILED':
|
|
111
|
+
throw decryptionFailedError
|
|
112
|
+
default:
|
|
113
|
+
throw e
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_parseEncryptionKeyFromDotenvKey (dotenvKey) {
|
|
119
|
+
// Parse DOTENV_KEY. Format is a URI
|
|
120
|
+
let uri
|
|
121
|
+
try {
|
|
122
|
+
uri = new URL(dotenvKey)
|
|
123
|
+
} catch (e) {
|
|
124
|
+
const code = 'INVALID_DOTENV_KEY'
|
|
125
|
+
const message = `INVALID_DOTENV_KEY: ${e.message}`
|
|
126
|
+
const error = new Error(message)
|
|
127
|
+
error.code = code
|
|
128
|
+
|
|
129
|
+
throw error
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Get decrypt key
|
|
133
|
+
const key = uri.password
|
|
134
|
+
if (!key) {
|
|
135
|
+
const code = 'INVALID_DOTENV_KEY'
|
|
136
|
+
const message = 'INVALID_DOTENV_KEY: Missing key part'
|
|
137
|
+
const error = new Error(message)
|
|
138
|
+
error.code = code
|
|
139
|
+
|
|
140
|
+
throw error
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return Buffer.from(key.slice(-64), 'hex')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_hash (str) {
|
|
147
|
+
return xxhash.h32(str, XXHASH_SEED).toString(16)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = DotenvVault
|
package/src/lib/main.js
CHANGED
|
@@ -3,6 +3,7 @@ const dotenv = require('dotenv')
|
|
|
3
3
|
const dotenvExpand = require('dotenv-expand')
|
|
4
4
|
|
|
5
5
|
// services
|
|
6
|
+
const Encrypt = require('./services/encrypt')
|
|
6
7
|
const Ls = require('./services/ls')
|
|
7
8
|
|
|
8
9
|
const config = function (options) {
|
|
@@ -105,6 +106,10 @@ const inject = function (processEnv = {}, parsed = {}, overload = false) {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
const encrypt = function (directory, envFile) {
|
|
110
|
+
return new Encrypt(directory, envFile).run()
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
const ls = function (directory, envFile) {
|
|
109
114
|
return new Ls(directory, envFile).run()
|
|
110
115
|
}
|
|
@@ -116,5 +121,6 @@ module.exports = {
|
|
|
116
121
|
parse,
|
|
117
122
|
parseExpand,
|
|
118
123
|
inject,
|
|
119
|
-
ls
|
|
124
|
+
ls,
|
|
125
|
+
encrypt
|
|
120
126
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const dotenv = require('dotenv')
|
|
4
|
+
|
|
5
|
+
const DotenvKeys = require('./../helpers/dotenvKeys')
|
|
6
|
+
const DotenvVault = require('./../helpers/dotenvVault')
|
|
7
|
+
|
|
8
|
+
const ENCODING = 'utf8'
|
|
9
|
+
|
|
10
|
+
class Encrypt {
|
|
11
|
+
constructor (directory = '.', envFile = '.env') {
|
|
12
|
+
this.directory = directory
|
|
13
|
+
this.envFile = envFile
|
|
14
|
+
// calculated
|
|
15
|
+
this.envKeysFilepath = path.resolve(this.directory, '.env.keys')
|
|
16
|
+
this.envVaultFilepath = path.resolve(this.directory, '.env.vault')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
run () {
|
|
20
|
+
if (this.envFile.length < 1) {
|
|
21
|
+
const code = 'MISSING_ENV_FILES'
|
|
22
|
+
const message = 'no .env* files found'
|
|
23
|
+
const help = '? add one with [echo "HELLO=World" > .env] and then run [dotenvx encrypt]'
|
|
24
|
+
|
|
25
|
+
const error = new Error(message)
|
|
26
|
+
error.code = code
|
|
27
|
+
error.help = help
|
|
28
|
+
throw error
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parsedDotenvKeys = this._parsedDotenvKeys()
|
|
32
|
+
const parsedDotenvVaults = this._parsedDotenvVault()
|
|
33
|
+
const envFilepaths = this._envFilepaths()
|
|
34
|
+
|
|
35
|
+
// build filepaths to be passed to DotenvKeys
|
|
36
|
+
const uniqueEnvFilepaths = new Set()
|
|
37
|
+
for (const envFilepath of envFilepaths) {
|
|
38
|
+
const filepath = path.resolve(this.directory, envFilepath)
|
|
39
|
+
if (!fs.existsSync(filepath)) {
|
|
40
|
+
const code = 'MISSING_ENV_FILE'
|
|
41
|
+
const message = `file does not exist at [${filepath}]`
|
|
42
|
+
const help = `? add it with [echo "HELLO=World" > ${envFilepath}] and then run [dotenvx encrypt]`
|
|
43
|
+
|
|
44
|
+
const error = new Error(message)
|
|
45
|
+
error.code = code
|
|
46
|
+
error.help = help
|
|
47
|
+
throw error
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
uniqueEnvFilepaths.add(filepath)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// generate .env.keys string
|
|
54
|
+
const {
|
|
55
|
+
dotenvKeys,
|
|
56
|
+
dotenvKeysFile,
|
|
57
|
+
addedKeys,
|
|
58
|
+
existingKeys
|
|
59
|
+
} = new DotenvKeys([...uniqueEnvFilepaths], parsedDotenvKeys).run()
|
|
60
|
+
|
|
61
|
+
// build look up of .env filepaths and their raw content
|
|
62
|
+
const dotenvFiles = {}
|
|
63
|
+
for (const filepath of [...uniqueEnvFilepaths]) {
|
|
64
|
+
const raw = fs.readFileSync(filepath, ENCODING)
|
|
65
|
+
dotenvFiles[filepath] = raw
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// generate .env.vault string
|
|
69
|
+
const {
|
|
70
|
+
dotenvVaultFile,
|
|
71
|
+
addedVaults,
|
|
72
|
+
existingVaults,
|
|
73
|
+
addedDotenvFilenames
|
|
74
|
+
} = new DotenvVault(dotenvFiles, dotenvKeys, parsedDotenvVaults).run()
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
// from DotenvKeys
|
|
78
|
+
dotenvKeys,
|
|
79
|
+
dotenvKeysFile,
|
|
80
|
+
addedKeys,
|
|
81
|
+
existingKeys,
|
|
82
|
+
// from DotenvVault
|
|
83
|
+
dotenvVaultFile,
|
|
84
|
+
addedVaults,
|
|
85
|
+
existingVaults,
|
|
86
|
+
addedDotenvFilenames
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_envFilepaths () {
|
|
91
|
+
if (!Array.isArray(this.envFile)) {
|
|
92
|
+
return [this.envFile]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return this.envFile
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_parsedDotenvKeys () {
|
|
99
|
+
const options = {
|
|
100
|
+
path: this.envKeysFilepath
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return dotenv.configDotenv(options).parsed
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_parsedDotenvVault () {
|
|
107
|
+
const options = {
|
|
108
|
+
path: this.envVaultFilepath
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return dotenv.configDotenv(options).parsed
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = Encrypt
|