@dotenvx/dotenvx 0.7.3 → 0.8.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 +10 -0
- package/package.json +10 -1
- package/src/cli/actions/encrypt.js +168 -0
- package/src/cli/actions/hub/login.js +105 -0
- package/src/cli/actions/hub/open.js +33 -0
- package/src/cli/actions/hub/push.js +111 -0
- package/src/cli/actions/hub/status.js +8 -0
- package/src/cli/actions/hub/token.js +9 -0
- package/src/cli/actions/run.js +108 -0
- package/src/cli/commands/hub.js +39 -0
- package/src/cli/dotenvx.js +11 -259
- package/src/cli/examples.js +4 -4
- package/src/cli/helpers.js +37 -0
- package/src/lib/main.js +9 -9
- package/src/shared/createSpinner.js +15 -0
- package/src/shared/logger.js +53 -1
- package/src/shared/store.js +101 -0
package/README.md
CHANGED
|
@@ -112,6 +112,16 @@ More examples
|
|
|
112
112
|
Hello World
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
</details>
|
|
116
|
+
* <details><summary>Bash 🖥️</summary><br>
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
$ echo "HELLO=World" > .env
|
|
120
|
+
|
|
121
|
+
$ dotenvx run --quiet -- sh -c 'echo $HELLO'
|
|
122
|
+
World
|
|
123
|
+
```
|
|
124
|
+
|
|
115
125
|
</details>
|
|
116
126
|
* <details><summary>Frameworks ▲</summary><br>
|
|
117
127
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.8.0",
|
|
3
3
|
"name": "@dotenvx/dotenvx",
|
|
4
4
|
"description": "a better dotenv–from the creator of `dotenv`",
|
|
5
5
|
"author": "@motdotla",
|
|
@@ -24,8 +24,17 @@
|
|
|
24
24
|
"test": "jest --verbose"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"@inquirer/prompts": "^3.3.0",
|
|
28
|
+
"axios": "^1.6.2",
|
|
29
|
+
"chalk": "^4.1.2",
|
|
30
|
+
"clipboardy": "^2.3.0",
|
|
27
31
|
"commander": "^11.1.0",
|
|
32
|
+
"conf": "^10.2.0",
|
|
28
33
|
"dotenv": "^16.3.1",
|
|
34
|
+
"open": "^8.4.2",
|
|
35
|
+
"ora": "^5.4.1",
|
|
36
|
+
"qrcode-terminal": "^0.12.0",
|
|
37
|
+
"update-notifier": "^5.1.0",
|
|
29
38
|
"execa": "^5.1.1",
|
|
30
39
|
"winston": "^3.11.0",
|
|
31
40
|
"xxhashjs": "^0.2.2"
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
|
|
3
|
+
const main = require('./../../lib/main')
|
|
4
|
+
const logger = require('./../../shared/logger')
|
|
5
|
+
const helpers = require('./../helpers')
|
|
6
|
+
const createSpinner = require('./../../shared/createSpinner')
|
|
7
|
+
|
|
8
|
+
const spinner = createSpinner('encrypting')
|
|
9
|
+
|
|
10
|
+
// constants
|
|
11
|
+
const ENCODING = 'utf8'
|
|
12
|
+
|
|
13
|
+
async function encrypt () {
|
|
14
|
+
spinner.start()
|
|
15
|
+
await helpers.sleep(500) // better dx
|
|
16
|
+
|
|
17
|
+
const options = this.opts()
|
|
18
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
19
|
+
|
|
20
|
+
let optionEnvFile = options.envFile
|
|
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
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
logger.verbose(`generating .env.keys from ${optionEnvFile}`)
|
|
38
|
+
|
|
39
|
+
const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
|
|
40
|
+
|
|
41
|
+
for (const envFilepath of optionEnvFile) {
|
|
42
|
+
const filepath = helpers.resolvePath(envFilepath)
|
|
43
|
+
if (!fs.existsSync(filepath)) {
|
|
44
|
+
spinner.fail(`file does not exist at [${filepath}]`)
|
|
45
|
+
logger.help(`? add it with [echo "HELLO=World" > ${envFilepath}] and then run [dotenvx encrypt]`)
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const environment = helpers.guessEnvironment(filepath)
|
|
50
|
+
const key = `DOTENV_KEY_${environment.toUpperCase()}`
|
|
51
|
+
|
|
52
|
+
let value = dotenvKeys[key]
|
|
53
|
+
|
|
54
|
+
// first time seeing new DOTENV_KEY_${environment}
|
|
55
|
+
if (!value || value.length === 0) {
|
|
56
|
+
logger.verbose(`generating ${key}`)
|
|
57
|
+
value = helpers.generateDotenvKey(environment)
|
|
58
|
+
logger.debug(`generating ${key} as ${value}`)
|
|
59
|
+
|
|
60
|
+
dotenvKeys[key] = value
|
|
61
|
+
|
|
62
|
+
addedKeys.add(key) // for info logging to user
|
|
63
|
+
} else {
|
|
64
|
+
logger.verbose(`existing ${key}`)
|
|
65
|
+
logger.debug(`existing ${key} as ${value}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let keysData = `#/!!!!!!!!!!!!!!!!!!!.env.keys!!!!!!!!!!!!!!!!!!!!!!/
|
|
70
|
+
#/ DOTENV_KEYs. DO NOT commit to source control /
|
|
71
|
+
#/ [how it works](https://dotenv.org/env-keys) /
|
|
72
|
+
#/--------------------------------------------------/\n`
|
|
73
|
+
|
|
74
|
+
for (const key in dotenvKeys) {
|
|
75
|
+
const value = dotenvKeys[key]
|
|
76
|
+
keysData += `${key}="${value}"\n`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fs.writeFileSync('.env.keys', keysData)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
spinner.fail(error.message)
|
|
82
|
+
process.exit(1)
|
|
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
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let vaultData = `#/-------------------.env.vault---------------------/
|
|
117
|
+
#/ cloud-agnostic vaulting standard /
|
|
118
|
+
#/ [how it works](https://dotenv.org/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`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fs.writeFileSync('.env.vault', vaultData)
|
|
129
|
+
} catch (e) {
|
|
130
|
+
spinner.fail(e.message)
|
|
131
|
+
process.exit(1)
|
|
132
|
+
}
|
|
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
|
+
|
|
154
|
+
// logger.verbose('')
|
|
155
|
+
// logger.verbose('next:')
|
|
156
|
+
// logger.verbose('')
|
|
157
|
+
// logger.verbose(' 1. commit .env.vault safely to code')
|
|
158
|
+
// logger.verbose(' 2. set DOTENV_KEY on server (or ci)')
|
|
159
|
+
// logger.verbose(' 3. push your code')
|
|
160
|
+
// logger.verbose('')
|
|
161
|
+
// logger.verbose('protips:')
|
|
162
|
+
// logger.verbose('')
|
|
163
|
+
// logger.verbose(' * .env.keys file holds your decryption DOTENV_KEYs')
|
|
164
|
+
// logger.verbose(' * DO NOT commit .env.keys to code')
|
|
165
|
+
// logger.verbose(' * share .env.keys file over secure channels only')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = encrypt
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const open = require('open')
|
|
2
|
+
const axios = require('axios')
|
|
3
|
+
const clipboardy = require('clipboardy')
|
|
4
|
+
const { confirm } = require('@inquirer/prompts')
|
|
5
|
+
|
|
6
|
+
const createSpinner = require('./../../../shared/createSpinner')
|
|
7
|
+
const store = require('./../../../shared/store')
|
|
8
|
+
const logger = require('./../../../shared/logger')
|
|
9
|
+
const helpers = require('./../../helpers')
|
|
10
|
+
|
|
11
|
+
const OAUTH_CLIENT_ID = 'oac_dotenvxcli'
|
|
12
|
+
|
|
13
|
+
const spinner = createSpinner('waiting on user authorization')
|
|
14
|
+
|
|
15
|
+
async function pollTokenUrl (tokenUrl, deviceCode, interval) {
|
|
16
|
+
logger.http(`POST ${tokenUrl} with deviceCode ${deviceCode} at interval ${interval}`)
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const response = await axios.post(tokenUrl, {
|
|
20
|
+
client_id: OAUTH_CLIENT_ID,
|
|
21
|
+
device_code: deviceCode,
|
|
22
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
logger.http(response.data)
|
|
26
|
+
|
|
27
|
+
if (response.data.access_token) {
|
|
28
|
+
spinner.start()
|
|
29
|
+
store.setToken(response.data.full_username, response.data.access_token)
|
|
30
|
+
store.setHostname(response.data.hostname)
|
|
31
|
+
spinner.succeed(`logged in as ${response.data.username}`)
|
|
32
|
+
process.exit(0)
|
|
33
|
+
} else {
|
|
34
|
+
// continue polling if no access_token. shouldn't ever get here it server is implemented correctly
|
|
35
|
+
setTimeout(() => pollTokenUrl(tokenUrl, deviceCode, interval), interval * 1000)
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error.response && error.response.data) {
|
|
39
|
+
logger.http(error.response.data)
|
|
40
|
+
|
|
41
|
+
// continue polling if authorization_pending
|
|
42
|
+
if (error.response.data.error === 'authorization_pending') {
|
|
43
|
+
setTimeout(() => pollTokenUrl(tokenUrl, deviceCode, interval), interval * 1000)
|
|
44
|
+
} else {
|
|
45
|
+
spinner.start()
|
|
46
|
+
spinner.fail(error.response.data.error_description)
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
spinner.start()
|
|
51
|
+
spinner.fail(error)
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function login () {
|
|
58
|
+
const options = this.opts()
|
|
59
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
60
|
+
|
|
61
|
+
const hostname = options.hostname
|
|
62
|
+
const deviceCodeUrl = `${hostname}/oauth/device/code`
|
|
63
|
+
const tokenUrl = `${hostname}/oauth/token`
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await axios.post(deviceCodeUrl, {
|
|
67
|
+
client_id: OAUTH_CLIENT_ID
|
|
68
|
+
})
|
|
69
|
+
const data = response.data
|
|
70
|
+
const deviceCode = data.device_code
|
|
71
|
+
const userCode = data.user_code
|
|
72
|
+
const verificationUri = data.verification_uri
|
|
73
|
+
const interval = data.interval
|
|
74
|
+
|
|
75
|
+
try { clipboardy.writeSync(userCode) } catch (_e) {}
|
|
76
|
+
|
|
77
|
+
// qrcode.generate(verificationUri, { small: true }) // too verbose
|
|
78
|
+
|
|
79
|
+
// begin polling
|
|
80
|
+
pollTokenUrl(tokenUrl, deviceCode, interval)
|
|
81
|
+
|
|
82
|
+
// optionally allow user to open browser
|
|
83
|
+
const answer = await confirm({ message: `press Enter to open [${verificationUri}] and enter code [${helpers.formatCode(userCode)}]...` })
|
|
84
|
+
|
|
85
|
+
if (answer) {
|
|
86
|
+
await open(verificationUri)
|
|
87
|
+
|
|
88
|
+
spinner.start()
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error.response && error.response.data) {
|
|
92
|
+
logger.http(error.response.data)
|
|
93
|
+
|
|
94
|
+
spinner.start()
|
|
95
|
+
spinner.fail(error.response.data.error_description)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
} else {
|
|
98
|
+
spinner.start()
|
|
99
|
+
spinner.fail(error.toString())
|
|
100
|
+
process.exit(1)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = login
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const openBrowser = require('open')
|
|
2
|
+
const { confirm } = require('@inquirer/prompts')
|
|
3
|
+
|
|
4
|
+
const createSpinner = require('./../../../shared/createSpinner')
|
|
5
|
+
const logger = require('./../../../shared/logger')
|
|
6
|
+
const helpers = require('./../../helpers')
|
|
7
|
+
|
|
8
|
+
async function open () {
|
|
9
|
+
const options = this.opts()
|
|
10
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
11
|
+
|
|
12
|
+
const hostname = options.hostname
|
|
13
|
+
const remoteOriginUrl = helpers.getRemoteOriginUrl()
|
|
14
|
+
const usernameName = helpers.extractUsernameName(remoteOriginUrl)
|
|
15
|
+
|
|
16
|
+
const openUrl = `${hostname}/gh/${usernameName}`
|
|
17
|
+
|
|
18
|
+
// optionally allow user to open browser
|
|
19
|
+
const answer = await confirm({ message: `press Enter to open [${openUrl}]...` })
|
|
20
|
+
|
|
21
|
+
if (answer) {
|
|
22
|
+
const spinner = createSpinner('opening')
|
|
23
|
+
spinner.start()
|
|
24
|
+
|
|
25
|
+
await helpers.sleep(500) // better dx
|
|
26
|
+
|
|
27
|
+
await openBrowser(openUrl)
|
|
28
|
+
|
|
29
|
+
spinner.succeed(`opened [${usernameName}]`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = open
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const { execSync } = require('child_process')
|
|
3
|
+
const axios = require('axios')
|
|
4
|
+
|
|
5
|
+
const store = require('./../../../shared/store')
|
|
6
|
+
const logger = require('./../../../shared/logger')
|
|
7
|
+
const helpers = require('./../../helpers')
|
|
8
|
+
const createSpinner = require('./../../../shared/createSpinner')
|
|
9
|
+
|
|
10
|
+
const spinner = createSpinner('pushing')
|
|
11
|
+
|
|
12
|
+
// constants
|
|
13
|
+
const ENCODING = 'utf8'
|
|
14
|
+
|
|
15
|
+
function isGitRepository () {
|
|
16
|
+
try {
|
|
17
|
+
// Redirect standard error to null to suppress Git error messages
|
|
18
|
+
const result = execSync('git rev-parse --is-inside-work-tree 2> /dev/null').toString().trim()
|
|
19
|
+
return result === 'true'
|
|
20
|
+
} catch (_error) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isGithub (url) {
|
|
26
|
+
return url.includes('github.com')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create a simple-git instance for the current directory
|
|
30
|
+
async function push () {
|
|
31
|
+
spinner.start()
|
|
32
|
+
await helpers.sleep(500) // better dx
|
|
33
|
+
|
|
34
|
+
const options = this.opts()
|
|
35
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
36
|
+
|
|
37
|
+
const hostname = options.hostname
|
|
38
|
+
const pushUrl = `${hostname}/v1/push`
|
|
39
|
+
const keysFilename = '.env.keys'
|
|
40
|
+
const vaultFilename = '.env.vault'
|
|
41
|
+
|
|
42
|
+
if (!isGitRepository()) {
|
|
43
|
+
spinner.fail('oops, must be a git repository')
|
|
44
|
+
logger.help('? create one with [git init .]')
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const remoteOriginUrl = helpers.getRemoteOriginUrl()
|
|
49
|
+
if (!remoteOriginUrl) {
|
|
50
|
+
spinner.fail('oops, must have a remote origin (git remote -v)')
|
|
51
|
+
logger.help('? create it at [github.com/new] and then run [git remote add origin git@github.com:username/repository.git]')
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!isGithub(remoteOriginUrl)) {
|
|
56
|
+
spinner.fail('oops, must be a github.com remote origin (git remote -v)')
|
|
57
|
+
logger.help('? create it at [github.com/new] and then run [git remote add origin git@github.com:username/repository.git]')
|
|
58
|
+
logger.help2('ℹ need support for other origins? [please tell us](https://github.com/dotenvx/dotenvx/issues)')
|
|
59
|
+
process.exit(1)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(keysFilename)) {
|
|
63
|
+
spinner.fail('oops, missing .env.keys file')
|
|
64
|
+
logger.help('? generate one with [dotenvx encrypt]')
|
|
65
|
+
logger.help2('ℹ a .env.keys file holds decryption keys for a .env.vault file')
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(vaultFilename)) {
|
|
70
|
+
spinner.fail('oops, missing .env.vault file')
|
|
71
|
+
logger.help('? generate one with [dotenvx encrypt]')
|
|
72
|
+
logger.help2('ℹ a .env.vault file holds encrypted secrets per environment')
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const oauthToken = store.getToken()
|
|
77
|
+
const dotenvKeysContent = fs.readFileSync(keysFilename, ENCODING)
|
|
78
|
+
const dotenvVaultContent = fs.readFileSync(vaultFilename, ENCODING)
|
|
79
|
+
const usernameName = helpers.extractUsernameName(remoteOriginUrl)
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const postData = {
|
|
83
|
+
username_name: usernameName,
|
|
84
|
+
DOTENV_KEYS: dotenvKeysContent,
|
|
85
|
+
DOTENV_VAULT: dotenvVaultContent
|
|
86
|
+
}
|
|
87
|
+
const options = {
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${oauthToken}`
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await axios.post(pushUrl, postData, options)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.response && error.response.data) {
|
|
95
|
+
logger.http(error.response.data)
|
|
96
|
+
spinner.fail(error.response.data.error.message)
|
|
97
|
+
if (error.response.status === 404) {
|
|
98
|
+
logger.help(`? try visiting [${hostname}gh/${usernameName}] in your browser`)
|
|
99
|
+
}
|
|
100
|
+
process.exit(1)
|
|
101
|
+
} else {
|
|
102
|
+
spinner.fail(error.toString())
|
|
103
|
+
process.exit(1)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
spinner.succeed(`pushed [${usernameName}]`)
|
|
108
|
+
logger.help2('ℹ run [dotenvx hub open] to view on hub')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = push
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const logger = require('./../../shared/logger')
|
|
3
|
+
const helpers = require('./../helpers')
|
|
4
|
+
const main = require('./../../lib/main')
|
|
5
|
+
|
|
6
|
+
const ENCODING = 'utf8'
|
|
7
|
+
|
|
8
|
+
async function run () {
|
|
9
|
+
const options = this.opts()
|
|
10
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
11
|
+
|
|
12
|
+
// load from .env.vault file
|
|
13
|
+
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
|
14
|
+
const filepath = helpers.resolvePath('.env.vault')
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(filepath)) {
|
|
17
|
+
logger.error(`you set DOTENV_KEY but your .env.vault file is missing: ${filepath}`)
|
|
18
|
+
} else {
|
|
19
|
+
logger.verbose(`loading env from encrypted ${filepath}`)
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
23
|
+
const parsedVault = main.parse(src)
|
|
24
|
+
|
|
25
|
+
logger.debug(`decrypting encrypted env from ${filepath}`)
|
|
26
|
+
// handle scenario for comma separated keys - for use with key rotation
|
|
27
|
+
// example: DOTENV_KEY="dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenv.org/vault/.env.vault?environment=prod"
|
|
28
|
+
const dotenvKeys = process.env.DOTENV_KEY.split(',')
|
|
29
|
+
const length = dotenvKeys.length
|
|
30
|
+
|
|
31
|
+
let decrypted
|
|
32
|
+
for (let i = 0; i < length; i++) {
|
|
33
|
+
try {
|
|
34
|
+
// Get full dotenvKey
|
|
35
|
+
const dotenvKey = dotenvKeys[i].trim()
|
|
36
|
+
|
|
37
|
+
const key = helpers._parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
38
|
+
const ciphertext = helpers._parseCipherTextFromDotenvKeyAndParsedVault(dotenvKey, parsedVault)
|
|
39
|
+
|
|
40
|
+
// Decrypt
|
|
41
|
+
decrypted = main.decrypt(ciphertext, key)
|
|
42
|
+
|
|
43
|
+
break
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// last key
|
|
46
|
+
if (i + 1 >= length) {
|
|
47
|
+
throw error
|
|
48
|
+
}
|
|
49
|
+
// try next key
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
logger.debug(decrypted)
|
|
53
|
+
const parsed = main.parse(decrypted)
|
|
54
|
+
const result = main.inject(process.env, parsed, options.overload)
|
|
55
|
+
|
|
56
|
+
logger.successv(`injecting env (${result.injected.size}) from encrypted .env.vault`)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
logger.error(e)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
// convert to array if needed
|
|
63
|
+
let optionEnvFile = options.envFile
|
|
64
|
+
if (!Array.isArray(optionEnvFile)) {
|
|
65
|
+
optionEnvFile = [optionEnvFile]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const readableFilepaths = new Set()
|
|
69
|
+
const injected = new Set()
|
|
70
|
+
|
|
71
|
+
for (const envFilepath of optionEnvFile) {
|
|
72
|
+
const filepath = helpers.resolvePath(envFilepath)
|
|
73
|
+
|
|
74
|
+
logger.verbose(`loading env from ${filepath}`)
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
78
|
+
const parsed = main.parse(src)
|
|
79
|
+
const result = main.inject(process.env, parsed, options.overload)
|
|
80
|
+
|
|
81
|
+
readableFilepaths.add(envFilepath)
|
|
82
|
+
result.injected.forEach(key => injected.add(key))
|
|
83
|
+
} catch (e) {
|
|
84
|
+
logger.warn(e)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (readableFilepaths.size > 0) {
|
|
89
|
+
logger.successv(`injecting env (${injected.size}) from ${[...readableFilepaths]}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Extract command and arguments after '--'
|
|
94
|
+
const commandIndex = process.argv.indexOf('--')
|
|
95
|
+
if (commandIndex === -1 || commandIndex === process.argv.length - 1) {
|
|
96
|
+
logger.error('missing command after [dotenvx run --]')
|
|
97
|
+
logger.error('')
|
|
98
|
+
logger.error(' get help: [dotenvx help run]')
|
|
99
|
+
logger.error(' or try: [dotenvx run -- npm run dev]')
|
|
100
|
+
process.exit(1)
|
|
101
|
+
} else {
|
|
102
|
+
const subCommand = process.argv.slice(commandIndex + 1)
|
|
103
|
+
|
|
104
|
+
await helpers.executeCommand(subCommand, process.env)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = run
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { Command } = require('commander')
|
|
2
|
+
|
|
3
|
+
const store = require('./../../shared/store')
|
|
4
|
+
|
|
5
|
+
const hub = new Command('hub')
|
|
6
|
+
|
|
7
|
+
hub
|
|
8
|
+
.description('Interact with the hub')
|
|
9
|
+
|
|
10
|
+
hub
|
|
11
|
+
.command('login')
|
|
12
|
+
.description('authenticate to dotenvx hub')
|
|
13
|
+
.option('-h, --hostname <url>', 'set hostname', store.getHostname())
|
|
14
|
+
.action(require('./../actions/hub/login'))
|
|
15
|
+
|
|
16
|
+
hub
|
|
17
|
+
.command('push')
|
|
18
|
+
.description('push .env.keys to dotenvx hub')
|
|
19
|
+
.option('-h, --hostname <url>', 'set hostname', store.getHostname())
|
|
20
|
+
.action(require('./../actions/hub/push'))
|
|
21
|
+
|
|
22
|
+
hub
|
|
23
|
+
.command('open')
|
|
24
|
+
.description('view repository on dotenvx hub')
|
|
25
|
+
.option('-h, --hostname <url>', 'set hostname', store.getHostname())
|
|
26
|
+
.action(require('./../actions/hub/open'))
|
|
27
|
+
|
|
28
|
+
hub
|
|
29
|
+
.command('token')
|
|
30
|
+
.description('print the auth token dotenvx hub is configured to use')
|
|
31
|
+
.option('-h, --hostname <url>', 'set hostname', 'https://hub.dotenvx.com')
|
|
32
|
+
.action(require('./../actions/hub/token'))
|
|
33
|
+
|
|
34
|
+
hub
|
|
35
|
+
.command('status')
|
|
36
|
+
.description('display logged in user')
|
|
37
|
+
.action(require('./../actions/hub/status'))
|
|
38
|
+
|
|
39
|
+
module.exports = hub
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const updateNotifier = require('update-notifier')
|
|
4
4
|
const { Command } = require('commander')
|
|
5
5
|
const program = new Command()
|
|
6
6
|
|
|
7
|
-
// constants
|
|
8
|
-
const ENCODING = 'utf8'
|
|
9
|
-
|
|
10
7
|
const logger = require('./../shared/logger')
|
|
11
8
|
const helpers = require('./helpers')
|
|
12
9
|
const examples = require('./examples')
|
|
13
10
|
const { AppendToIgnores } = require('./ignores')
|
|
14
11
|
const packageJson = require('./../shared/packageJson')
|
|
15
|
-
|
|
12
|
+
|
|
13
|
+
// once a day check for any updates
|
|
14
|
+
const notifier = updateNotifier({ pkg: packageJson })
|
|
15
|
+
if (notifier.update) {
|
|
16
|
+
logger.warn(`Update available ${notifier.update.current} → ${notifier.update.latest} [see changelog](dotenvx.com/changelog)`)
|
|
17
|
+
}
|
|
16
18
|
|
|
17
19
|
// global log levels
|
|
18
20
|
program
|
|
@@ -58,266 +60,16 @@ program.command('run')
|
|
|
58
60
|
.addHelpText('after', examples.run)
|
|
59
61
|
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)', '.env')
|
|
60
62
|
.option('-o, --overload', 'override existing env variables')
|
|
61
|
-
.action(
|
|
62
|
-
const options = this.opts()
|
|
63
|
-
logger.debug('configuring options')
|
|
64
|
-
logger.debug(options)
|
|
65
|
-
|
|
66
|
-
// load from .env.vault file
|
|
67
|
-
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
|
68
|
-
const filepath = helpers.resolvePath('.env.vault')
|
|
69
|
-
|
|
70
|
-
if (!fs.existsSync(filepath)) {
|
|
71
|
-
logger.error(`you set DOTENV_KEY but your .env.vault file is missing: ${filepath}`)
|
|
72
|
-
} else {
|
|
73
|
-
logger.verbose(`loading env from encrypted ${filepath}`)
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
logger.debug(`reading encrypted env from ${filepath}`)
|
|
77
|
-
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
78
|
-
|
|
79
|
-
logger.debug(`parsing encrypted env from ${filepath}`)
|
|
80
|
-
const parsedVault = main.parse(src)
|
|
81
|
-
|
|
82
|
-
logger.debug(`decrypting encrypted env from ${filepath}`)
|
|
83
|
-
// handle scenario for comma separated keys - for use with key rotation
|
|
84
|
-
// example: DOTENV_KEY="dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenv.org/vault/.env.vault?environment=prod"
|
|
85
|
-
const dotenvKeys = process.env.DOTENV_KEY.split(',')
|
|
86
|
-
const length = dotenvKeys.length
|
|
87
|
-
|
|
88
|
-
let decrypted
|
|
89
|
-
for (let i = 0; i < length; i++) {
|
|
90
|
-
try {
|
|
91
|
-
// Get full dotenvKey
|
|
92
|
-
const dotenvKey = dotenvKeys[i].trim()
|
|
93
|
-
|
|
94
|
-
const key = helpers._parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
95
|
-
const ciphertext = helpers._parseCipherTextFromDotenvKeyAndParsedVault(dotenvKey, parsedVault)
|
|
96
|
-
|
|
97
|
-
// Decrypt
|
|
98
|
-
decrypted = main.decrypt(ciphertext, key)
|
|
99
|
-
|
|
100
|
-
break
|
|
101
|
-
} catch (error) {
|
|
102
|
-
// last key
|
|
103
|
-
if (i + 1 >= length) {
|
|
104
|
-
throw error
|
|
105
|
-
}
|
|
106
|
-
// try next key
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
logger.debug(decrypted)
|
|
110
|
-
|
|
111
|
-
logger.debug(`parsing decrypted env from ${filepath}`)
|
|
112
|
-
const parsed = main.parse(decrypted)
|
|
113
|
-
|
|
114
|
-
logger.debug(`writing decrypted env from ${filepath}`)
|
|
115
|
-
const result = main.write(process.env, parsed, options.overload)
|
|
116
|
-
|
|
117
|
-
logger.info(`loading env (${result.written.size}) from encrypted .env.vault`)
|
|
118
|
-
} catch (e) {
|
|
119
|
-
logger.error(e)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
} else {
|
|
123
|
-
// convert to array if needed
|
|
124
|
-
let optionEnvFile = options.envFile
|
|
125
|
-
if (!Array.isArray(optionEnvFile)) {
|
|
126
|
-
optionEnvFile = [optionEnvFile]
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const readableFilepaths = new Set()
|
|
130
|
-
const written = new Set()
|
|
131
|
-
|
|
132
|
-
for (const envFilepath of optionEnvFile) {
|
|
133
|
-
const filepath = helpers.resolvePath(envFilepath)
|
|
134
|
-
|
|
135
|
-
logger.verbose(`loading env from ${filepath}`)
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
logger.debug(`reading env from ${filepath}`)
|
|
139
|
-
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
140
|
-
|
|
141
|
-
logger.debug(`parsing env from ${filepath}`)
|
|
142
|
-
const parsed = main.parse(src)
|
|
143
|
-
|
|
144
|
-
logger.debug(`writing env from ${filepath}`)
|
|
145
|
-
const result = main.write(process.env, parsed, options.overload)
|
|
146
|
-
|
|
147
|
-
readableFilepaths.add(envFilepath)
|
|
148
|
-
result.written.forEach(key => written.add(key))
|
|
149
|
-
} catch (e) {
|
|
150
|
-
logger.warn(e)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (readableFilepaths.size > 0) {
|
|
155
|
-
logger.info(`loading env (${written.size}) from ${[...readableFilepaths]}`)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Extract command and arguments after '--'
|
|
160
|
-
const commandIndex = process.argv.indexOf('--')
|
|
161
|
-
if (commandIndex === -1 || commandIndex === process.argv.length - 1) {
|
|
162
|
-
logger.error('missing command after [dotenvx run --]')
|
|
163
|
-
logger.error('')
|
|
164
|
-
logger.error(' get help: [dotenvx help run]')
|
|
165
|
-
logger.error(' or try: [dotenvx run -- npm run dev]')
|
|
166
|
-
process.exit(1)
|
|
167
|
-
} else {
|
|
168
|
-
const subCommand = process.argv.slice(commandIndex + 1)
|
|
169
|
-
|
|
170
|
-
await helpers.executeCommand(subCommand, process.env)
|
|
171
|
-
}
|
|
172
|
-
})
|
|
63
|
+
.action(require('./actions/run'))
|
|
173
64
|
|
|
174
65
|
// dotenvx encrypt
|
|
175
66
|
program.command('encrypt')
|
|
176
67
|
.description('encrypt .env.* to .env.vault')
|
|
177
68
|
.addHelpText('after', examples.encrypt)
|
|
178
69
|
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)', helpers.findEnvFiles('./'))
|
|
179
|
-
.action(
|
|
180
|
-
const options = this.opts()
|
|
181
|
-
logger.debug('configuring options')
|
|
182
|
-
logger.debug(options)
|
|
183
|
-
|
|
184
|
-
let optionEnvFile = options.envFile
|
|
185
|
-
if (!Array.isArray(optionEnvFile)) {
|
|
186
|
-
optionEnvFile = [optionEnvFile]
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const addedKeys = new Set()
|
|
190
|
-
const addedVaults = new Set()
|
|
191
|
-
const addedEnvFilepaths = new Set()
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
logger.verbose(`generating .env.keys from ${optionEnvFile}`)
|
|
70
|
+
.action(require('./actions/encrypt'))
|
|
195
71
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
for (const envFilepath of optionEnvFile) {
|
|
199
|
-
const filepath = helpers.resolvePath(envFilepath)
|
|
200
|
-
if (!fs.existsSync(filepath)) {
|
|
201
|
-
throw new Error(`file does not exist: ${filepath}`)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const environment = helpers.guessEnvironment(filepath)
|
|
205
|
-
const key = `DOTENV_KEY_${environment.toUpperCase()}`
|
|
206
|
-
|
|
207
|
-
let value = dotenvKeys[key]
|
|
208
|
-
|
|
209
|
-
// first time seeing new DOTENV_KEY_${environment}
|
|
210
|
-
if (!value || value.length === 0) {
|
|
211
|
-
logger.verbose(`generating ${key}`)
|
|
212
|
-
value = helpers.generateDotenvKey(environment)
|
|
213
|
-
logger.debug(`generating ${key} as ${value}`)
|
|
214
|
-
|
|
215
|
-
dotenvKeys[key] = value
|
|
216
|
-
|
|
217
|
-
addedKeys.add(key) // for info logging to user
|
|
218
|
-
} else {
|
|
219
|
-
logger.verbose(`existing ${key}`)
|
|
220
|
-
logger.debug(`existing ${key} as ${value}`)
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
let keysData = `#/!!!!!!!!!!!!!!!!!!!.env.keys!!!!!!!!!!!!!!!!!!!!!!/
|
|
225
|
-
#/ DOTENV_KEYs. DO NOT commit to source control /
|
|
226
|
-
#/ [how it works](https://dotenv.org/env-keys) /
|
|
227
|
-
#/--------------------------------------------------/\n`
|
|
228
|
-
|
|
229
|
-
for (const key in dotenvKeys) {
|
|
230
|
-
const value = dotenvKeys[key]
|
|
231
|
-
keysData += `${key}="${value}"\n`
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
fs.writeFileSync('.env.keys', keysData)
|
|
235
|
-
} catch (e) {
|
|
236
|
-
logger.error(e)
|
|
237
|
-
process.exit(1)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// used later in logging to user
|
|
241
|
-
const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
logger.verbose(`generating .env.vault from ${optionEnvFile}`)
|
|
245
|
-
|
|
246
|
-
const dotenvVaults = (main.configDotenv({ path: '.env.vault' }).parsed || {})
|
|
247
|
-
|
|
248
|
-
for (const envFilepath of optionEnvFile) {
|
|
249
|
-
const filepath = helpers.resolvePath(envFilepath)
|
|
250
|
-
const environment = helpers.guessEnvironment(filepath)
|
|
251
|
-
const vault = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
252
|
-
|
|
253
|
-
let ciphertext = dotenvVaults[vault]
|
|
254
|
-
const dotenvKey = dotenvKeys[`DOTENV_KEY_${environment.toUpperCase()}`]
|
|
255
|
-
|
|
256
|
-
if (!ciphertext || ciphertext.length === 0 || helpers.changed(ciphertext, dotenvKey, filepath, ENCODING)) {
|
|
257
|
-
logger.verbose(`encrypting ${vault}`)
|
|
258
|
-
ciphertext = helpers.encryptFile(filepath, dotenvKey, ENCODING)
|
|
259
|
-
logger.verbose(`encrypting ${vault} as ${ciphertext}`)
|
|
260
|
-
|
|
261
|
-
dotenvVaults[vault] = ciphertext
|
|
262
|
-
|
|
263
|
-
addedVaults.add(vault) // for info logging to user
|
|
264
|
-
addedEnvFilepaths.add(envFilepath) // for info logging to user
|
|
265
|
-
} else {
|
|
266
|
-
logger.verbose(`existing ${vault}`)
|
|
267
|
-
logger.debug(`existing ${vault} as ${ciphertext}`)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
let vaultData = `#/-------------------.env.vault---------------------/
|
|
272
|
-
#/ cloud-agnostic vaulting standard /
|
|
273
|
-
#/ [how it works](https://dotenv.org/env-vault) /
|
|
274
|
-
#/--------------------------------------------------/\n\n`
|
|
275
|
-
|
|
276
|
-
for (const vault in dotenvVaults) {
|
|
277
|
-
const value = dotenvVaults[vault]
|
|
278
|
-
const environment = vault.replace('DOTENV_VAULT_', '').toLowerCase()
|
|
279
|
-
vaultData += `# ${environment}\n`
|
|
280
|
-
vaultData += `${vault}="${value}"\n\n`
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
fs.writeFileSync('.env.vault', vaultData)
|
|
284
|
-
} catch (e) {
|
|
285
|
-
logger.error(e)
|
|
286
|
-
process.exit(1)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (addedEnvFilepaths.size > 0) {
|
|
290
|
-
logger.info(`encrypted to .env.vault (${[...addedEnvFilepaths]})`)
|
|
291
|
-
} else {
|
|
292
|
-
logger.info(`no changes (${optionEnvFile})`)
|
|
293
|
-
}
|
|
294
|
-
if (addedKeys.size > 0) {
|
|
295
|
-
logger.info(`${helpers.pluralize('key', addedKeys.size)} added to .env.keys (${[...addedKeys]})`)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (addedVaults.size > 0) {
|
|
299
|
-
const DOTENV_VAULT_X = [...addedVaults][addedVaults.size - 1]
|
|
300
|
-
const DOTENV_KEY_X = DOTENV_VAULT_X.replace('_VAULT_', '_KEY_')
|
|
301
|
-
const tryKey = dotenvKeys[DOTENV_KEY_X] || '<dotenv_key_environment>'
|
|
302
|
-
|
|
303
|
-
logger.info('')
|
|
304
|
-
logger.info('next, try it:')
|
|
305
|
-
logger.info('')
|
|
306
|
-
logger.info(` [DOTENV_KEY='${tryKey}' dotenvx run -- your-cmd]`)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
logger.verbose('')
|
|
310
|
-
logger.verbose('next:')
|
|
311
|
-
logger.verbose('')
|
|
312
|
-
logger.verbose(' 1. commit .env.vault safely to code')
|
|
313
|
-
logger.verbose(' 2. set DOTENV_KEY on server (or ci)')
|
|
314
|
-
logger.verbose(' 3. push your code')
|
|
315
|
-
logger.verbose('')
|
|
316
|
-
logger.verbose('tips:')
|
|
317
|
-
logger.verbose('')
|
|
318
|
-
logger.verbose(' * .env.keys file holds your decryption DOTENV_KEYs')
|
|
319
|
-
logger.verbose(' * DO NOT commit .env.keys to code')
|
|
320
|
-
logger.verbose(' * share .env.keys file over secure channels only')
|
|
321
|
-
})
|
|
72
|
+
// dotenvx hub
|
|
73
|
+
program.addCommand(require('./commands/hub'))
|
|
322
74
|
|
|
323
75
|
program.parse(process.argv)
|
package/src/cli/examples.js
CHANGED
|
@@ -16,7 +16,7 @@ Try it:
|
|
|
16
16
|
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
|
|
17
17
|
|
|
18
18
|
$ dotenvx run -- node index.js
|
|
19
|
-
[dotenvx]
|
|
19
|
+
[dotenvx] injecting env (1) from .env
|
|
20
20
|
Hello World
|
|
21
21
|
\`\`\`
|
|
22
22
|
`
|
|
@@ -38,11 +38,11 @@ Try it:
|
|
|
38
38
|
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
|
|
39
39
|
|
|
40
40
|
$ dotenvx encrypt
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
encrypted to .env.vault (.env,.env.production)
|
|
42
|
+
keys added to .env.keys (DOTENV_KEY_PRODUCTION,DOTENV_KEY_PRODUCTION)
|
|
43
43
|
|
|
44
44
|
$ DOTENV_KEY='<dotenv_key_production>' dotenvx run -- node index.js
|
|
45
|
-
[dotenvx]
|
|
45
|
+
[dotenvx] injecting env (1) from encrypted .env.vault
|
|
46
46
|
Hello production
|
|
47
47
|
\`\`\`
|
|
48
48
|
`
|
package/src/cli/helpers.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const execa = require('execa')
|
|
4
4
|
const crypto = require('crypto')
|
|
5
|
+
const { execSync } = require('child_process')
|
|
5
6
|
const xxhash = require('xxhashjs')
|
|
6
7
|
|
|
7
8
|
const XXHASH_SEED = 0xABCD
|
|
@@ -13,6 +14,10 @@ const logger = require('./../shared/logger')
|
|
|
13
14
|
const RESERVED_ENV_FILES = ['.env.vault', '.env.projects', '.env.keys', '.env.me', '.env.x']
|
|
14
15
|
const REPORT_ISSUE_LINK = 'https://github.com/dotenvx/dotenvx/issues/new'
|
|
15
16
|
|
|
17
|
+
const sleep = function (ms) {
|
|
18
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
// resolve path based on current running process location
|
|
17
22
|
const resolvePath = function (filepath) {
|
|
18
23
|
return path.resolve(process.cwd(), filepath)
|
|
@@ -205,7 +210,36 @@ const _parseCipherTextFromDotenvKeyAndParsedVault = function (dotenvKey, parsedV
|
|
|
205
210
|
return ciphertext
|
|
206
211
|
}
|
|
207
212
|
|
|
213
|
+
const formatCode = function (str) {
|
|
214
|
+
const parts = []
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < str.length; i += 4) {
|
|
217
|
+
parts.push(str.substring(i, i + 4))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return parts.join('-')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const getRemoteOriginUrl = function () {
|
|
224
|
+
try {
|
|
225
|
+
const url = execSync('git remote get-url origin 2> /dev/null').toString().trim()
|
|
226
|
+
return url
|
|
227
|
+
} catch (_error) {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const extractUsernameName = function (url) {
|
|
233
|
+
// Removing the protocol part and splitting by slashes and colons
|
|
234
|
+
// Removing the protocol part and .git suffix, then splitting by slashes and colons
|
|
235
|
+
const parts = url.replace(/(^\w+:|^)\/\//, '').replace(/\.git$/, '').split(/[/:]/)
|
|
236
|
+
|
|
237
|
+
// Extract the 'username/repository' part
|
|
238
|
+
return parts.slice(-2).join('/')
|
|
239
|
+
}
|
|
240
|
+
|
|
208
241
|
module.exports = {
|
|
242
|
+
sleep,
|
|
209
243
|
resolvePath,
|
|
210
244
|
executeCommand,
|
|
211
245
|
pluralize,
|
|
@@ -216,6 +250,9 @@ module.exports = {
|
|
|
216
250
|
encrypt,
|
|
217
251
|
changed,
|
|
218
252
|
hash,
|
|
253
|
+
formatCode,
|
|
254
|
+
getRemoteOriginUrl,
|
|
255
|
+
extractUsernameName,
|
|
219
256
|
_parseEncryptionKeyFromDotenvKey,
|
|
220
257
|
_parseCipherTextFromDotenvKeyAndParsedVault
|
|
221
258
|
}
|
package/src/lib/main.js
CHANGED
|
@@ -21,12 +21,12 @@ const parse = function (src) {
|
|
|
21
21
|
return result
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const inject = function (processEnv = {}, parsed = {}, overload = false) {
|
|
25
25
|
if (typeof parsed !== 'object') {
|
|
26
|
-
throw new Error('OBJECT_REQUIRED: Please check the parsed argument being passed to
|
|
26
|
+
throw new Error('OBJECT_REQUIRED: Please check the parsed argument being passed to inject')
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
const injected = new Set()
|
|
30
30
|
const preExisting = new Set()
|
|
31
31
|
|
|
32
32
|
// set processEnv
|
|
@@ -34,19 +34,19 @@ const write = function (processEnv = {}, parsed = {}, overload = false) {
|
|
|
34
34
|
if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
|
|
35
35
|
if (overload === true) {
|
|
36
36
|
processEnv[key] = parsed[key]
|
|
37
|
-
|
|
37
|
+
injected.add(key)
|
|
38
38
|
|
|
39
39
|
logger.verbose(`${key} set`)
|
|
40
40
|
logger.debug(`${key} set to ${parsed[key]}`)
|
|
41
41
|
} else {
|
|
42
42
|
preExisting.add(key)
|
|
43
43
|
|
|
44
|
-
logger.verbose(`${key} pre-exists`)
|
|
45
|
-
logger.debug(`${key} pre-exists as ${processEnv[key]}`)
|
|
44
|
+
logger.verbose(`${key} pre-exists (protip: use --overload to override)`)
|
|
45
|
+
logger.debug(`${key} pre-exists as ${processEnv[key]} (protip: use --overload to override)`)
|
|
46
46
|
}
|
|
47
47
|
} else {
|
|
48
48
|
processEnv[key] = parsed[key]
|
|
49
|
-
|
|
49
|
+
injected.add(key)
|
|
50
50
|
|
|
51
51
|
logger.verbose(`${key} set`)
|
|
52
52
|
logger.debug(`${key} set to ${parsed[key]}`)
|
|
@@ -54,7 +54,7 @@ const write = function (processEnv = {}, parsed = {}, overload = false) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
return {
|
|
57
|
-
|
|
57
|
+
injected,
|
|
58
58
|
preExisting
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -64,5 +64,5 @@ module.exports = {
|
|
|
64
64
|
configDotenv,
|
|
65
65
|
decrypt,
|
|
66
66
|
parse,
|
|
67
|
-
|
|
67
|
+
inject
|
|
68
68
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const ora = require('ora')
|
|
2
|
+
const chalk = require('chalk')
|
|
3
|
+
|
|
4
|
+
const createSpinner = function (initialMessage = '') {
|
|
5
|
+
const spinner = ora(initialMessage)
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
start: (message) => spinner.start(message),
|
|
9
|
+
succeed: (message) => spinner.succeed(chalk.keyword('green')(message)),
|
|
10
|
+
done: (message) => spinner.succeed(message),
|
|
11
|
+
fail: (message) => spinner.fail(chalk.bold.red(message))
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = createSpinner
|
package/src/shared/logger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const winston = require('winston')
|
|
2
|
+
const chalk = require('chalk')
|
|
2
3
|
|
|
3
4
|
const printf = winston.format.printf
|
|
4
5
|
const combine = winston.format.combine
|
|
@@ -7,14 +8,65 @@ const transports = winston.transports
|
|
|
7
8
|
|
|
8
9
|
const packageJson = require('./packageJson')
|
|
9
10
|
|
|
11
|
+
const levels = {
|
|
12
|
+
error: 0,
|
|
13
|
+
warn: 1,
|
|
14
|
+
success: 2,
|
|
15
|
+
successv: 2,
|
|
16
|
+
info: 2,
|
|
17
|
+
help: 2,
|
|
18
|
+
help2: 2,
|
|
19
|
+
blank: 2,
|
|
20
|
+
http: 3,
|
|
21
|
+
verbose: 4,
|
|
22
|
+
debug: 5,
|
|
23
|
+
silly: 6
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const error = chalk.bold.red
|
|
27
|
+
const warn = chalk.keyword('orangered')
|
|
28
|
+
const success = chalk.keyword('green')
|
|
29
|
+
const successv = chalk.keyword('olive') // yellow-ish tint that 'looks' like dotenv
|
|
30
|
+
const help = chalk.keyword('blue')
|
|
31
|
+
const help2 = chalk.keyword('gray')
|
|
32
|
+
const http = chalk.keyword('green')
|
|
33
|
+
const verbose = chalk.keyword('plum')
|
|
34
|
+
const debug = chalk.keyword('plum')
|
|
35
|
+
|
|
10
36
|
const dotenvxFormat = printf(({ level, message, label, timestamp }) => {
|
|
11
37
|
const formattedMessage = typeof message === 'object' ? JSON.stringify(message) : message
|
|
12
38
|
|
|
13
|
-
|
|
39
|
+
switch (level.toLowerCase()) {
|
|
40
|
+
case 'error':
|
|
41
|
+
return error(formattedMessage)
|
|
42
|
+
case 'warn':
|
|
43
|
+
return warn(formattedMessage)
|
|
44
|
+
case 'success':
|
|
45
|
+
return success(formattedMessage)
|
|
46
|
+
case 'successv': // success with 'version'
|
|
47
|
+
return successv(`[dotenvx@${packageJson.version}] ${formattedMessage}`)
|
|
48
|
+
case 'info':
|
|
49
|
+
return formattedMessage
|
|
50
|
+
case 'help':
|
|
51
|
+
return help(formattedMessage)
|
|
52
|
+
case 'help2':
|
|
53
|
+
return help2(formattedMessage)
|
|
54
|
+
case 'http':
|
|
55
|
+
return http(formattedMessage)
|
|
56
|
+
case 'verbose':
|
|
57
|
+
return verbose(formattedMessage)
|
|
58
|
+
case 'debug':
|
|
59
|
+
return debug(formattedMessage)
|
|
60
|
+
case 'blank': // custom
|
|
61
|
+
return formattedMessage
|
|
62
|
+
default: // handle uncaught
|
|
63
|
+
return formattedMessage
|
|
64
|
+
}
|
|
14
65
|
})
|
|
15
66
|
|
|
16
67
|
const logger = createLogger({
|
|
17
68
|
level: 'info',
|
|
69
|
+
levels,
|
|
18
70
|
format: combine(
|
|
19
71
|
dotenvxFormat
|
|
20
72
|
),
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const Conf = require('conf')
|
|
2
|
+
const main = require('./../lib/main')
|
|
3
|
+
|
|
4
|
+
function jsonToEnv (json) {
|
|
5
|
+
return Object.entries(json).map(function ([key, value]) {
|
|
6
|
+
return key + '=' + `"${value}"`
|
|
7
|
+
}).join('\n')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function convertFullUsernameToEnvFormat (fullUsername) {
|
|
11
|
+
// gh/motdotla => GH_MOTDOTLA_DOTENVX_TOKEN
|
|
12
|
+
return fullUsername
|
|
13
|
+
.toUpperCase()
|
|
14
|
+
.replace(/\//g, '_') // Replace all slashes with underscores
|
|
15
|
+
.concat('_DOTENVX_TOKEN') // Append '_DOTENVX_TOKEN' at the end
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function findFirstMatchingKey (data) {
|
|
19
|
+
const dotenvxTokenValue = data.DOTENVX_TOKEN
|
|
20
|
+
|
|
21
|
+
for (const [key, value] of Object.entries(data)) {
|
|
22
|
+
if (key !== 'DOTENVX_TOKEN' && value === dotenvxTokenValue) {
|
|
23
|
+
return key
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null // Return null if no matching key is found
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseUsernameFromTokenKey (key) {
|
|
31
|
+
// Remove the leading GH_/GL_ and trailing '_DOTENVX_TOKEN'
|
|
32
|
+
const modifiedKey = key.replace(/^(GH_|GL_)/, '').replace(/_DOTENVX_TOKEN$/, '')
|
|
33
|
+
|
|
34
|
+
// Convert to lowercase
|
|
35
|
+
return modifiedKey.toLowerCase()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const confStore = new Conf({
|
|
39
|
+
projectName: 'dotenvx',
|
|
40
|
+
configName: '.env',
|
|
41
|
+
// looks better on user's machine
|
|
42
|
+
// https://github.com/sindresorhus/conf/tree/v10.2.0#projectsuffix.
|
|
43
|
+
projectSuffix: '',
|
|
44
|
+
fileExtension: '',
|
|
45
|
+
// in the spirit of dotenv and format inherently puts limits on config complexity
|
|
46
|
+
serialize: function (json) {
|
|
47
|
+
return jsonToEnv(json)
|
|
48
|
+
},
|
|
49
|
+
// Convert .env format to an object
|
|
50
|
+
deserialize: function (env) {
|
|
51
|
+
return main.parse(env)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const getHostname = function () {
|
|
56
|
+
return confStore.get('DOTENVX_HOSTNAME') || 'https://hub.dotenvx.com'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const getUsername = function () {
|
|
60
|
+
const key = findFirstMatchingKey(confStore.store)
|
|
61
|
+
|
|
62
|
+
if (key) {
|
|
63
|
+
return parseUsernameFromTokenKey(key)
|
|
64
|
+
} else {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const getToken = function () {
|
|
70
|
+
return confStore.get('DOTENVX_TOKEN')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const setToken = function (fullUsername, accessToken) {
|
|
74
|
+
// current logged in user
|
|
75
|
+
confStore.set('DOTENVX_TOKEN', accessToken)
|
|
76
|
+
|
|
77
|
+
// for future use to switch between accounts locally
|
|
78
|
+
const memory = convertFullUsernameToEnvFormat(fullUsername)
|
|
79
|
+
confStore.set(memory, accessToken)
|
|
80
|
+
|
|
81
|
+
return accessToken
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const setHostname = function (hostname) {
|
|
85
|
+
confStore.set('DOTENVX_HOSTNAME', hostname)
|
|
86
|
+
|
|
87
|
+
return hostname
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const configPath = function () {
|
|
91
|
+
return confStore.path
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
getHostname,
|
|
96
|
+
getToken,
|
|
97
|
+
getUsername,
|
|
98
|
+
setHostname,
|
|
99
|
+
setToken,
|
|
100
|
+
configPath
|
|
101
|
+
}
|