@dotenvx/dotenvx 0.16.1 → 0.17.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.
- package/package.json +3 -3
- package/src/cli/actions/decrypt.js +2 -1
- package/src/cli/actions/encrypt.js +62 -121
- package/src/cli/actions/hub/login.js +11 -2
- package/src/cli/actions/run.js +96 -3
- package/src/cli/dotenvx.js +2 -2
- package/src/cli/helpers.js +3 -216
- package/src/lib/helpers/dotenvKeys.js +68 -0
- package/src/lib/helpers/dotenvVault.js +151 -0
- package/src/lib/helpers/parseEncryptionKeyFromDotenvKey.js +19 -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.1",
|
|
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",
|
|
@@ -4,6 +4,7 @@ const main = require('./../../lib/main')
|
|
|
4
4
|
const logger = require('./../../shared/logger')
|
|
5
5
|
const helpers = require('./../helpers')
|
|
6
6
|
const createSpinner = require('./../../shared/createSpinner')
|
|
7
|
+
const parseEncryptionKeyFromDotenvKey = require('./../../lib/helpers/parseEncryptionKeyFromDotenvKey')
|
|
7
8
|
|
|
8
9
|
const spinner = createSpinner('decrypting')
|
|
9
10
|
|
|
@@ -50,7 +51,7 @@ async function decrypt () {
|
|
|
50
51
|
|
|
51
52
|
// give warning if not found
|
|
52
53
|
if (ciphertext && ciphertext.length >= 1) {
|
|
53
|
-
const key =
|
|
54
|
+
const key = parseEncryptionKeyFromDotenvKey(value.trim())
|
|
54
55
|
|
|
55
56
|
// Decrypt
|
|
56
57
|
const decrypted = main.decrypt(ciphertext, key)
|
|
@@ -1,155 +1,96 @@
|
|
|
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')
|
|
5
6
|
const helpers = require('./../helpers')
|
|
6
7
|
const createSpinner = require('./../../shared/createSpinner')
|
|
7
|
-
|
|
8
8
|
const spinner = createSpinner('encrypting')
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const RESERVED_ENV_FILES = ['.env.vault', '.env.project', '.env.keys', '.env.me', '.env.x']
|
|
11
|
+
|
|
12
|
+
const findEnvFiles = function (directory) {
|
|
13
|
+
const files = fs.readdirSync(directory)
|
|
14
|
+
|
|
15
|
+
const envFiles = files.filter(file =>
|
|
16
|
+
file.startsWith('.env') &&
|
|
17
|
+
!file.endsWith('.previous') &&
|
|
18
|
+
!RESERVED_ENV_FILES.includes(file)
|
|
19
|
+
)
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
return envFiles
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function encrypt (directory) {
|
|
14
25
|
spinner.start()
|
|
15
26
|
await helpers.sleep(500) // better dx
|
|
16
27
|
|
|
28
|
+
logger.debug(`directory: ${directory}`)
|
|
29
|
+
|
|
17
30
|
const options = this.opts()
|
|
18
31
|
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
19
32
|
|
|
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
|
-
}
|
|
33
|
+
const optionEnvFile = options.envFile || findEnvFiles(directory)
|
|
35
34
|
|
|
36
35
|
try {
|
|
37
|
-
|
|
36
|
+
const {
|
|
37
|
+
dotenvKeys,
|
|
38
|
+
dotenvKeysFile,
|
|
39
|
+
addedKeys,
|
|
40
|
+
existingKeys,
|
|
41
|
+
dotenvVaultFile,
|
|
42
|
+
addedVaults,
|
|
43
|
+
existingVaults,
|
|
44
|
+
addedDotenvFilenames
|
|
45
|
+
} = main.encrypt(directory, optionEnvFile)
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const environment = helpers.guessEnvironment(filepath)
|
|
50
|
-
const key = `DOTENV_KEY_${environment.toUpperCase()}`
|
|
51
|
-
|
|
52
|
-
let value = dotenvKeys[key]
|
|
47
|
+
logger.verbose(`generating .env.keys from ${optionEnvFile}`)
|
|
48
|
+
if (addedKeys.length > 0) {
|
|
49
|
+
logger.verbose(`generated ${addedKeys}`)
|
|
50
|
+
}
|
|
51
|
+
if (existingKeys.length > 0) {
|
|
52
|
+
logger.verbose(`existing ${existingKeys}`)
|
|
53
|
+
}
|
|
54
|
+
fs.writeFileSync(path.resolve(directory, '.env.keys'), dotenvKeysFile)
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
logger.verbose(`generating .env.vault from ${optionEnvFile}`)
|
|
57
|
+
if (addedVaults.length > 0) {
|
|
58
|
+
logger.verbose(`encrypting ${addedVaults}`)
|
|
59
|
+
}
|
|
60
|
+
if (existingVaults.length > 0) {
|
|
61
|
+
logger.verbose(`existing ${existingVaults}`)
|
|
62
|
+
}
|
|
63
|
+
fs.writeFileSync(path.resolve(directory, '.env.vault'), dotenvVaultFile)
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
if (addedDotenvFilenames.length > 0) {
|
|
66
|
+
spinner.succeed(`encrypted to .env.vault (${addedDotenvFilenames})`)
|
|
67
|
+
logger.help2('ℹ commit .env.vault to code: [git commit -am ".env.vault"]')
|
|
68
|
+
} else {
|
|
69
|
+
spinner.done(`no changes (${optionEnvFile})`)
|
|
70
|
+
}
|
|
61
71
|
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
logger.debug(`existing ${key} as ${value}`)
|
|
66
|
-
}
|
|
72
|
+
if (addedKeys.length > 0) {
|
|
73
|
+
spinner.succeed(`${helpers.pluralize('key', addedKeys.length)} added to .env.keys (${addedKeys})`)
|
|
74
|
+
logger.help2('ℹ push .env.keys up to hub: [dotenvx hub push]')
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
if (addedVaults.length > 0) {
|
|
78
|
+
const DOTENV_VAULT_X = addedVaults[addedVaults.length - 1]
|
|
79
|
+
const DOTENV_KEY_X = DOTENV_VAULT_X.replace('_VAULT_', '_KEY_')
|
|
80
|
+
const tryKey = dotenvKeys[DOTENV_KEY_X]
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
const value = dotenvKeys[key]
|
|
76
|
-
keysData += `${key}="${value}"\n`
|
|
82
|
+
logger.help2(`ℹ run [DOTENV_KEY='${tryKey}' dotenvx run -- yourcommand] to test decryption locally`)
|
|
77
83
|
}
|
|
78
|
-
|
|
79
|
-
fs.writeFileSync('.env.keys', keysData)
|
|
80
84
|
} catch (error) {
|
|
81
85
|
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
|
-
}
|
|
86
|
+
if (error.help) {
|
|
87
|
+
logger.help(error.help)
|
|
114
88
|
}
|
|
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`
|
|
89
|
+
if (error.code) {
|
|
90
|
+
logger.debug(`ERROR_CODE: ${error.code}`)
|
|
126
91
|
}
|
|
127
|
-
|
|
128
|
-
fs.writeFileSync('.env.vault', vaultData)
|
|
129
|
-
} catch (e) {
|
|
130
|
-
spinner.fail(e.message)
|
|
131
92
|
process.exit(1)
|
|
132
93
|
}
|
|
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
94
|
}
|
|
154
95
|
|
|
155
96
|
module.exports = encrypt
|
|
@@ -6,12 +6,21 @@ const { confirm } = require('@inquirer/prompts')
|
|
|
6
6
|
const createSpinner = require('./../../../shared/createSpinner')
|
|
7
7
|
const store = require('./../../../shared/store')
|
|
8
8
|
const logger = require('./../../../shared/logger')
|
|
9
|
-
const helpers = require('./../../helpers')
|
|
10
9
|
|
|
11
10
|
const OAUTH_CLIENT_ID = 'oac_dotenvxcli'
|
|
12
11
|
|
|
13
12
|
const spinner = createSpinner('waiting on user authorization')
|
|
14
13
|
|
|
14
|
+
const formatCode = function (str) {
|
|
15
|
+
const parts = []
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < str.length; i += 4) {
|
|
18
|
+
parts.push(str.substring(i, i + 4))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return parts.join('-')
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
async function pollTokenUrl (tokenUrl, deviceCode, interval) {
|
|
16
25
|
logger.http(`POST ${tokenUrl} with deviceCode ${deviceCode} at interval ${interval}`)
|
|
17
26
|
|
|
@@ -100,7 +109,7 @@ async function login () {
|
|
|
100
109
|
pollTokenUrl(tokenUrl, deviceCode, interval)
|
|
101
110
|
|
|
102
111
|
// optionally allow user to open browser
|
|
103
|
-
const answer = await confirm({ message: `press Enter to open [${verificationUri}] and enter code [${
|
|
112
|
+
const answer = await confirm({ message: `press Enter to open [${verificationUri}] and enter code [${formatCode(userCode)}]...` })
|
|
104
113
|
|
|
105
114
|
if (answer) {
|
|
106
115
|
await open(verificationUri)
|
package/src/cli/actions/run.js
CHANGED
|
@@ -1,9 +1,102 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
+
const execa = require('execa')
|
|
2
3
|
const logger = require('./../../shared/logger')
|
|
3
4
|
const helpers = require('./../helpers')
|
|
4
5
|
const main = require('./../../lib/main')
|
|
6
|
+
const parseEncryptionKeyFromDotenvKey = require('./../../lib/helpers/parseEncryptionKeyFromDotenvKey')
|
|
5
7
|
|
|
6
8
|
const ENCODING = 'utf8'
|
|
9
|
+
const REPORT_ISSUE_LINK = 'https://github.com/dotenvx/dotenvx/issues/new'
|
|
10
|
+
|
|
11
|
+
const executeCommand = async function (commandArgs, env) {
|
|
12
|
+
const signals = [
|
|
13
|
+
'SIGHUP', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT',
|
|
14
|
+
'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM'
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
logger.debug(`executing process command [${commandArgs.join(' ')}]`)
|
|
18
|
+
|
|
19
|
+
// handler for SIGINT
|
|
20
|
+
let commandProcess
|
|
21
|
+
const sigintHandler = () => {
|
|
22
|
+
logger.debug('received SIGINT')
|
|
23
|
+
logger.debug('checking command process')
|
|
24
|
+
logger.debug(commandProcess)
|
|
25
|
+
|
|
26
|
+
if (commandProcess) {
|
|
27
|
+
logger.debug('sending SIGINT to command process')
|
|
28
|
+
commandProcess.kill('SIGINT') // Send SIGINT to the command process
|
|
29
|
+
} else {
|
|
30
|
+
logger.debug('no command process to send SIGINT to')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handleOtherSignal = (signal) => {
|
|
35
|
+
logger.debug(`received ${signal}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
commandProcess = execa(commandArgs[0], commandArgs.slice(1), {
|
|
40
|
+
stdio: 'inherit',
|
|
41
|
+
env: { ...process.env, ...env }
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
process.on('SIGINT', sigintHandler)
|
|
45
|
+
|
|
46
|
+
signals.forEach(signal => {
|
|
47
|
+
process.on(signal, () => handleOtherSignal(signal))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Wait for the command process to finish
|
|
51
|
+
const { exitCode } = await commandProcess
|
|
52
|
+
|
|
53
|
+
if (exitCode !== 0) {
|
|
54
|
+
logger.debug(`received exitCode ${exitCode}`)
|
|
55
|
+
throw new Error(`Command failed with exit code ${exitCode}`)
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.signal !== 'SIGINT') {
|
|
59
|
+
logger.error(error.message)
|
|
60
|
+
logger.error(`command [${commandArgs.join(' ')}] failed`)
|
|
61
|
+
logger.error('')
|
|
62
|
+
logger.error(` try without dotenvx: [${commandArgs.join(' ')}]`)
|
|
63
|
+
logger.error('')
|
|
64
|
+
logger.error('if that succeeds, then dotenvx is the culprit. report issue:')
|
|
65
|
+
logger.error(`<${REPORT_ISSUE_LINK}>`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Exit with the error code from the command process, or 1 if unavailable
|
|
69
|
+
process.exit(error.exitCode || 1)
|
|
70
|
+
} finally {
|
|
71
|
+
// Clean up: Remove the SIGINT handler
|
|
72
|
+
process.removeListener('SIGINT', sigintHandler)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const _parseCipherTextFromDotenvKeyAndParsedVault = function (dotenvKey, parsedVault) {
|
|
77
|
+
// Parse DOTENV_KEY. Format is a URI
|
|
78
|
+
let uri
|
|
79
|
+
try {
|
|
80
|
+
uri = new URL(dotenvKey)
|
|
81
|
+
} catch (e) {
|
|
82
|
+
throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get environment
|
|
86
|
+
const environment = uri.searchParams.get('environment')
|
|
87
|
+
if (!environment) {
|
|
88
|
+
throw new Error('INVALID_DOTENV_KEY: Missing environment part')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Get ciphertext payload
|
|
92
|
+
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
93
|
+
const ciphertext = parsedVault[environmentKey] // DOTENV_VAULT_PRODUCTION
|
|
94
|
+
if (!ciphertext) {
|
|
95
|
+
throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: cannot locate environment ${environmentKey} in your .env.vault file`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return ciphertext
|
|
99
|
+
}
|
|
7
100
|
|
|
8
101
|
async function run () {
|
|
9
102
|
const commandArgs = this.args
|
|
@@ -38,8 +131,8 @@ async function run () {
|
|
|
38
131
|
// Get full dotenvKey
|
|
39
132
|
const dotenvKey = dotenvKeys[i].trim()
|
|
40
133
|
|
|
41
|
-
const key =
|
|
42
|
-
const ciphertext =
|
|
134
|
+
const key = parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
135
|
+
const ciphertext = _parseCipherTextFromDotenvKeyAndParsedVault(dotenvKey, parsedVault)
|
|
43
136
|
|
|
44
137
|
// Decrypt
|
|
45
138
|
decrypted = main.decrypt(ciphertext, key)
|
|
@@ -124,7 +217,7 @@ async function run () {
|
|
|
124
217
|
process.exit(1)
|
|
125
218
|
} else {
|
|
126
219
|
// const commandArgs = process.argv.slice(commandIndex + 1)
|
|
127
|
-
await
|
|
220
|
+
await executeCommand(commandArgs, process.env)
|
|
128
221
|
}
|
|
129
222
|
}
|
|
130
223
|
|
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
|
package/src/cli/helpers.js
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
const fs = require('fs')
|
|
2
1
|
const path = require('path')
|
|
3
|
-
const
|
|
4
|
-
const crypto = require('crypto')
|
|
5
|
-
const { execSync } = require('child_process')
|
|
6
|
-
const xxhash = require('xxhashjs')
|
|
7
|
-
|
|
8
|
-
const XXHASH_SEED = 0xABCD
|
|
9
|
-
const NONCE_BYTES = 12
|
|
10
|
-
|
|
11
|
-
const main = require('./../lib/main')
|
|
12
|
-
const logger = require('./../shared/logger')
|
|
13
|
-
|
|
14
|
-
const RESERVED_ENV_FILES = ['.env.vault', '.env.projects', '.env.keys', '.env.me', '.env.x']
|
|
15
|
-
const REPORT_ISSUE_LINK = 'https://github.com/dotenvx/dotenvx/issues/new'
|
|
2
|
+
const childProcess = require('child_process')
|
|
16
3
|
|
|
17
4
|
const sleep = function (ms) {
|
|
18
5
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
@@ -23,71 +10,6 @@ const resolvePath = function (filepath) {
|
|
|
23
10
|
return path.resolve(process.cwd(), filepath)
|
|
24
11
|
}
|
|
25
12
|
|
|
26
|
-
const executeCommand = async function (commandArgs, env) {
|
|
27
|
-
const signals = [
|
|
28
|
-
'SIGHUP', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT',
|
|
29
|
-
'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM'
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
logger.debug(`executing process command [${commandArgs.join(' ')}]`)
|
|
33
|
-
|
|
34
|
-
// handler for SIGINT
|
|
35
|
-
let commandProcess
|
|
36
|
-
const sigintHandler = () => {
|
|
37
|
-
logger.debug('received SIGINT')
|
|
38
|
-
logger.debug('checking command process')
|
|
39
|
-
logger.debug(commandProcess)
|
|
40
|
-
|
|
41
|
-
if (commandProcess) {
|
|
42
|
-
logger.debug('sending SIGINT to command process')
|
|
43
|
-
commandProcess.kill('SIGINT') // Send SIGINT to the command process
|
|
44
|
-
} else {
|
|
45
|
-
logger.debug('no command process to send SIGINT to')
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const handleOtherSignal = (signal) => {
|
|
50
|
-
logger.debug(`received ${signal}`)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
commandProcess = execa(commandArgs[0], commandArgs.slice(1), {
|
|
55
|
-
stdio: 'inherit',
|
|
56
|
-
env: { ...process.env, ...env }
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
process.on('SIGINT', sigintHandler)
|
|
60
|
-
|
|
61
|
-
signals.forEach(signal => {
|
|
62
|
-
process.on(signal, () => handleOtherSignal(signal))
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
// Wait for the command process to finish
|
|
66
|
-
const { exitCode } = await commandProcess
|
|
67
|
-
|
|
68
|
-
if (exitCode !== 0) {
|
|
69
|
-
logger.debug(`received exitCode ${exitCode}`)
|
|
70
|
-
throw new Error(`Command failed with exit code ${exitCode}`)
|
|
71
|
-
}
|
|
72
|
-
} catch (error) {
|
|
73
|
-
if (error.signal !== 'SIGINT') {
|
|
74
|
-
logger.error(error.message)
|
|
75
|
-
logger.error(`command [${commandArgs.join(' ')}] failed`)
|
|
76
|
-
logger.error('')
|
|
77
|
-
logger.error(` try without dotenvx: [${commandArgs.join(' ')}]`)
|
|
78
|
-
logger.error('')
|
|
79
|
-
logger.error('if that succeeds, then dotenvx is the culprit. report issue:')
|
|
80
|
-
logger.error(`<${REPORT_ISSUE_LINK}>`)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Exit with the error code from the command process, or 1 if unavailable
|
|
84
|
-
process.exit(error.exitCode || 1)
|
|
85
|
-
} finally {
|
|
86
|
-
// Clean up: Remove the SIGINT handler
|
|
87
|
-
process.removeListener('SIGINT', sigintHandler)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
13
|
const pluralize = function (word, count) {
|
|
92
14
|
// simple pluralization: add 's' at the end
|
|
93
15
|
if (count === 0 || count > 1) {
|
|
@@ -97,133 +19,9 @@ const pluralize = function (word, count) {
|
|
|
97
19
|
}
|
|
98
20
|
}
|
|
99
21
|
|
|
100
|
-
const findEnvFiles = function (directory) {
|
|
101
|
-
const files = fs.readdirSync(directory)
|
|
102
|
-
|
|
103
|
-
const envFiles = files.filter(file =>
|
|
104
|
-
file.startsWith('.env') &&
|
|
105
|
-
!file.endsWith('.previous') &&
|
|
106
|
-
!RESERVED_ENV_FILES.includes(file)
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
return envFiles
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const guessEnvironment = function (filepath) {
|
|
113
|
-
const filename = path.basename(filepath)
|
|
114
|
-
const parts = filename.split('.')
|
|
115
|
-
const possibleEnvironment = parts[2] // ['', 'env', environment', 'previous']
|
|
116
|
-
|
|
117
|
-
if (!possibleEnvironment || possibleEnvironment.length === 0) {
|
|
118
|
-
return 'development'
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return possibleEnvironment
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const generateDotenvKey = function (environment) {
|
|
125
|
-
const rand = crypto.randomBytes(32).toString('hex')
|
|
126
|
-
|
|
127
|
-
return `dotenv://:key_${rand}@dotenvx.com/vault/.env.vault?environment=${environment.toLowerCase()}`
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const encryptFile = function (filepath, dotenvKey, encoding) {
|
|
131
|
-
const key = _parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
132
|
-
const message = fs.readFileSync(filepath, encoding)
|
|
133
|
-
|
|
134
|
-
const ciphertext = encrypt(key, message)
|
|
135
|
-
|
|
136
|
-
return ciphertext
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const encrypt = function (key, message) {
|
|
140
|
-
// set up nonce
|
|
141
|
-
const nonce = crypto.randomBytes(NONCE_BYTES)
|
|
142
|
-
|
|
143
|
-
// set up cipher
|
|
144
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
|
|
145
|
-
|
|
146
|
-
// generate ciphertext
|
|
147
|
-
let ciphertext = ''
|
|
148
|
-
ciphertext += cipher.update(message, 'utf8', 'hex')
|
|
149
|
-
ciphertext += cipher.final('hex')
|
|
150
|
-
ciphertext += cipher.getAuthTag().toString('hex')
|
|
151
|
-
|
|
152
|
-
// prepend nonce
|
|
153
|
-
ciphertext = nonce.toString('hex') + ciphertext
|
|
154
|
-
|
|
155
|
-
// base64 encode output
|
|
156
|
-
return Buffer.from(ciphertext, 'hex').toString('base64')
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const changed = function (ciphertext, dotenvKey, filepath, encoding) {
|
|
160
|
-
const key = _parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
161
|
-
const decrypted = main.decrypt(ciphertext, key)
|
|
162
|
-
const raw = fs.readFileSync(filepath, encoding)
|
|
163
|
-
|
|
164
|
-
return hash(decrypted) !== hash(raw)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const hash = function (str) {
|
|
168
|
-
return xxhash.h32(str, XXHASH_SEED).toString(16)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const _parseEncryptionKeyFromDotenvKey = function (dotenvKey) {
|
|
172
|
-
// Parse DOTENV_KEY. Format is a URI
|
|
173
|
-
let uri
|
|
174
|
-
try {
|
|
175
|
-
uri = new URL(dotenvKey)
|
|
176
|
-
} catch (e) {
|
|
177
|
-
throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Get decrypt key
|
|
181
|
-
const key = uri.password
|
|
182
|
-
if (!key) {
|
|
183
|
-
throw new Error('INVALID_DOTENV_KEY: Missing key part')
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return Buffer.from(key.slice(-64), 'hex')
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const _parseCipherTextFromDotenvKeyAndParsedVault = function (dotenvKey, parsedVault) {
|
|
190
|
-
// Parse DOTENV_KEY. Format is a URI
|
|
191
|
-
let uri
|
|
192
|
-
try {
|
|
193
|
-
uri = new URL(dotenvKey)
|
|
194
|
-
} catch (e) {
|
|
195
|
-
throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Get environment
|
|
199
|
-
const environment = uri.searchParams.get('environment')
|
|
200
|
-
if (!environment) {
|
|
201
|
-
throw new Error('INVALID_DOTENV_KEY: Missing environment part')
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Get ciphertext payload
|
|
205
|
-
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
206
|
-
const ciphertext = parsedVault[environmentKey] // DOTENV_VAULT_PRODUCTION
|
|
207
|
-
if (!ciphertext) {
|
|
208
|
-
throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: cannot locate environment ${environmentKey} in your .env.vault file`)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return ciphertext
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const formatCode = function (str) {
|
|
215
|
-
const parts = []
|
|
216
|
-
|
|
217
|
-
for (let i = 0; i < str.length; i += 4) {
|
|
218
|
-
parts.push(str.substring(i, i + 4))
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return parts.join('-')
|
|
222
|
-
}
|
|
223
|
-
|
|
224
22
|
const getRemoteOriginUrl = function () {
|
|
225
23
|
try {
|
|
226
|
-
const url = execSync('git remote get-url origin 2> /dev/null').toString().trim()
|
|
24
|
+
const url = childProcess.execSync('git remote get-url origin 2> /dev/null').toString().trim()
|
|
227
25
|
return url
|
|
228
26
|
} catch (_error) {
|
|
229
27
|
return null
|
|
@@ -242,18 +40,7 @@ const extractUsernameName = function (url) {
|
|
|
242
40
|
module.exports = {
|
|
243
41
|
sleep,
|
|
244
42
|
resolvePath,
|
|
245
|
-
executeCommand,
|
|
246
43
|
pluralize,
|
|
247
|
-
findEnvFiles,
|
|
248
|
-
guessEnvironment,
|
|
249
|
-
generateDotenvKey,
|
|
250
|
-
encryptFile,
|
|
251
|
-
encrypt,
|
|
252
|
-
changed,
|
|
253
|
-
hash,
|
|
254
|
-
formatCode,
|
|
255
44
|
getRemoteOriginUrl,
|
|
256
|
-
extractUsernameName
|
|
257
|
-
_parseEncryptionKeyFromDotenvKey,
|
|
258
|
-
_parseCipherTextFromDotenvKeyAndParsedVault
|
|
45
|
+
extractUsernameName
|
|
259
46
|
}
|
|
@@ -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
|
|
@@ -0,0 +1,19 @@
|
|
|
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: ${e.message}`)
|
|
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
|
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
|