@dotenvx/dotenvx 2.28.0 → 2.28.2
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/CHANGELOG.md +14 -1
- package/README.md +40 -8
- package/package.json +1 -1
- package/src/cli/actions/define.js +17 -0
- package/src/cli/actions/encrypt.js +1 -1
- package/src/cli/actions/ext/precommit.js +3 -0
- package/src/cli/actions/ext/precommitClean.js +21 -0
- package/src/cli/actions/get.js +2 -1
- package/src/cli/actions/init.js +3 -0
- package/src/cli/actions/lock/down.js +13 -32
- package/src/cli/actions/lock/up.js +13 -30
- package/src/cli/actions/run.js +2 -1
- package/src/cli/actions/set.js +1 -1
- package/src/cli/commands/custody.js +2 -2
- package/src/cli/commands/ext.js +3 -1
- package/src/cli/commands/fileOptions.js +22 -0
- package/src/cli/dotenvx.js +38 -21
- package/src/lib/custodians/index.js +73 -0
- package/src/lib/{helpers/bitwardenCustody.js → custodians/local/bitwarden.js} +12 -7
- package/src/lib/custodians/local/file.js +18 -0
- package/src/lib/custodians/local/native/backend.js +62 -0
- package/src/lib/custodians/local/native/index.js +28 -0
- package/src/lib/{helpers/storeNativePrivateKey.js → custodians/local/native/store.js} +3 -3
- package/src/lib/{helpers/onePasswordCustody.js → custodians/local/onepassword.js} +9 -5
- package/src/lib/custodians/lock.js +80 -0
- package/src/lib/{providers/armor/index.js → custodians/managed/armor/get.js} +6 -6
- package/src/lib/custodians/managed/armor/index.js +24 -0
- package/src/lib/custodians/managed/armor/store.js +37 -0
- package/src/lib/helpers/fsx.js +16 -5
- package/src/lib/helpers/installPrecommitFilter.js +50 -0
- package/src/lib/helpers/installPrecommitHook.js +2 -2
- package/src/lib/helpers/lockedValue.js +24 -0
- package/src/lib/helpers/macosKeychain.js +18 -2
- package/src/lib/helpers/matchesStoredKey.js +16 -0
- package/src/lib/helpers/normalizeDotenvConfigPath.js +1 -1
- package/src/lib/helpers/parseWithDecryptor.js +3 -0
- package/src/lib/helpers/prompts.js +18 -0
- package/src/lib/helpers/removeEnvKey.js +2 -1
- package/src/lib/helpers/resolveLockPassword.js +5 -0
- package/src/lib/helpers/selectKeyStorage.js +9 -21
- package/src/lib/helpers/unlockedValue.js +26 -0
- package/src/lib/helpers/upsertEnvKey.js +2 -1
- package/src/lib/helpers/withLockedKeys.js +41 -0
- package/src/lib/main.d.ts +6 -0
- package/src/lib/main.js +5 -2
- package/src/lib/providers/index.js +9 -66
- package/src/lib/providers/native/index.js +2 -62
- package/src/lib/providers/provider-worker.js +1 -1
- package/src/lib/resolvers/envs.js +14 -5
- package/src/lib/resolvers/get.js +1 -0
- package/src/lib/services/custodyTransfer.js +2 -2
- package/src/lib/services/init.js +49 -0
- package/src/lib/services/precommit.js +12 -1
- package/src/lib/services/validate.js +1 -0
- package/src/lib/transforms/encrypt.js +6 -49
- package/src/lib/transforms/set.js +6 -49
|
@@ -1,29 +1,15 @@
|
|
|
1
1
|
const prompts = require('./prompts')
|
|
2
|
-
const
|
|
3
|
-
const bitwardenCustody = require('./bitwardenCustody')
|
|
4
|
-
|
|
5
|
-
const secretStoreNames = {
|
|
6
|
-
darwin: 'macOS Keychain',
|
|
7
|
-
win32: 'Windows Credential Manager',
|
|
8
|
-
linux: 'Linux Secret Service'
|
|
9
|
-
}
|
|
2
|
+
const custodians = require('../custodians')
|
|
10
3
|
|
|
11
4
|
async function selectKeyStorage (options = {}) {
|
|
12
|
-
const
|
|
13
|
-
const defaultStorage = useNative ? 'native' : 'file'
|
|
5
|
+
const defaultStorage = custodians.get('native').enabled(options) ? 'native' : 'file'
|
|
14
6
|
if (process.env.CI || options.noCreate || !process.stdin.isTTY || !process.stderr.isTTY) return defaultStorage
|
|
15
7
|
|
|
16
|
-
const
|
|
17
|
-
const useBitwarden = !options.noBitwarden && process.env.DOTENVX_NO_BITWARDEN !== 'true' && await bitwardenCustody.available()
|
|
18
|
-
const localChoices = [
|
|
19
|
-
{ name: `OS${secretStoreNames[process.platform] ? ` (${secretStoreNames[process.platform]})` : ''}`, value: 'native', disabled: !useNative },
|
|
20
|
-
{ name: '1Password', value: 'onepassword', disabled: !use1Password },
|
|
21
|
-
{ name: 'Bitwarden', value: 'bitwarden', disabled: !useBitwarden },
|
|
22
|
-
{ name: 'File (.env.keys)', value: 'file', disabled: false }
|
|
23
|
-
]
|
|
8
|
+
const localChoices = await custodians.choices(options)
|
|
24
9
|
|
|
25
10
|
const choices = [{ name: '⛉ Local Custody', value: 'local', disabled: false }]
|
|
26
|
-
|
|
11
|
+
const managedChoices = (await custodians.choices(options, 'managed')).filter(choice => !choice.disabled).map(({ name, value }) => ({ name, value }))
|
|
12
|
+
if (managedChoices.length) choices.push({ name: '⛊ Managed Custody', value: 'managed' })
|
|
27
13
|
|
|
28
14
|
const context = { input: process.stdin, output: process.stderr }
|
|
29
15
|
const custody = await prompts.select({
|
|
@@ -31,10 +17,12 @@ async function selectKeyStorage (options = {}) {
|
|
|
31
17
|
choices
|
|
32
18
|
}, context)
|
|
33
19
|
|
|
34
|
-
|
|
20
|
+
const storage = await prompts.select({
|
|
35
21
|
message: custody === 'local' ? 'Choose local custody' : 'Choose managed custody',
|
|
36
|
-
choices: custody === 'local' ? localChoices :
|
|
22
|
+
choices: custody === 'local' ? localChoices : managedChoices
|
|
37
23
|
}, context)
|
|
24
|
+
|
|
25
|
+
return storage
|
|
38
26
|
}
|
|
39
27
|
|
|
40
28
|
module.exports = selectKeyStorage
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
|
|
3
|
+
function unlockedValue (lockedPrivateKey, passphrase) {
|
|
4
|
+
const parts = lockedPrivateKey.split(':')
|
|
5
|
+
const payload = Buffer.from(parts.slice(2).join(':'), 'base64url')
|
|
6
|
+
const version = payload.subarray(0, 1)[0]
|
|
7
|
+
const salt = payload.subarray(1, 17)
|
|
8
|
+
const iv = payload.subarray(17, 29)
|
|
9
|
+
const tag = payload.subarray(29, 45)
|
|
10
|
+
const ciphertext = payload.subarray(45)
|
|
11
|
+
const key = crypto.scryptSync(passphrase, salt, 32)
|
|
12
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
|
|
13
|
+
|
|
14
|
+
if (version !== 1) {
|
|
15
|
+
throw new Error('unsupported locked private key version')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
decipher.setAuthTag(tag)
|
|
19
|
+
|
|
20
|
+
return Buffer.concat([
|
|
21
|
+
decipher.update(ciphertext),
|
|
22
|
+
decipher.final()
|
|
23
|
+
]).toString('utf8')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = unlockedValue
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const path = require('path')
|
|
3
|
+
const fsx = require('./fsx')
|
|
3
4
|
|
|
4
5
|
function escapeForRegex (value) {
|
|
5
6
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
@@ -46,7 +47,7 @@ function upsertEnvKey (key, value, keysFilepath = '.env.keys') {
|
|
|
46
47
|
const changed = created || nextSrc !== src
|
|
47
48
|
|
|
48
49
|
if (changed) {
|
|
49
|
-
|
|
50
|
+
fsx.writeKeyFileSync(resolvedKeysFilepath, nextSrc)
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
return {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const { scan } = require('@dotenvx/primitives')
|
|
3
|
+
const protection = require('../custodians/lock')
|
|
4
|
+
|
|
5
|
+
// Primitives already resolve raw file/environment keys first. This provider
|
|
6
|
+
// supplies locked keys only when the requested private key is still missing.
|
|
7
|
+
function withLockedKeys (options, sync = false) {
|
|
8
|
+
const candidates = []
|
|
9
|
+
const paths = Array.isArray(options.fk) ? options.fk : [options.fk || '.env.keys']
|
|
10
|
+
for (const filepath of paths) {
|
|
11
|
+
let src
|
|
12
|
+
try { src = fs.readFileSync(filepath, 'utf8') } catch { continue }
|
|
13
|
+
for (const [name, values] of Object.entries(scan(src).parsed)) {
|
|
14
|
+
if (name.startsWith('DOTENV_PRIVATE_KEY')) candidates.push(...values.flatMap(value => value.split(',')))
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Environment assignments take precedence over key files.
|
|
18
|
+
for (const [name, value] of Object.entries(options.processEnv || process.env)) {
|
|
19
|
+
if (name.startsWith('DOTENV_PRIVATE_KEY') && typeof value === 'string') candidates.push(...value.split(','))
|
|
20
|
+
}
|
|
21
|
+
if (!candidates.some(value => value.startsWith('locked:'))) return options
|
|
22
|
+
candidates.reverse()
|
|
23
|
+
function find (publicKey) {
|
|
24
|
+
return candidates.find(value => value.startsWith(`locked:${publicKey}:`))
|
|
25
|
+
}
|
|
26
|
+
const provider = options.provider
|
|
27
|
+
return {
|
|
28
|
+
...options,
|
|
29
|
+
provider: sync
|
|
30
|
+
? publicKey => {
|
|
31
|
+
const value = find(publicKey)
|
|
32
|
+
return value ? protection.unlockSync(publicKey, { [publicKey]: value }, options) : (provider ? provider(publicKey) : {})
|
|
33
|
+
}
|
|
34
|
+
: async publicKey => {
|
|
35
|
+
const value = find(publicKey)
|
|
36
|
+
return value ? protection.unlock(publicKey, { [publicKey]: value }, options) : (provider ? provider(publicKey) : {})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = withLockedKeys
|
package/src/lib/main.d.ts
CHANGED
|
@@ -50,6 +50,9 @@ export function parse<T extends DotenvParseOutput = DotenvParseOutput>(
|
|
|
50
50
|
): T;
|
|
51
51
|
|
|
52
52
|
export interface DotenvConfigOptions {
|
|
53
|
+
/** Password to unlock private keys in memory. Defaults to DOTENVX_LOCK_PASSWORD. */
|
|
54
|
+
lockPassword?: string;
|
|
55
|
+
|
|
53
56
|
/**
|
|
54
57
|
* Specify explicit env sources. When set, `path` and `convention` are ignored.
|
|
55
58
|
*
|
|
@@ -339,6 +342,9 @@ export function set(
|
|
|
339
342
|
): Promise<SetOutput>;
|
|
340
343
|
|
|
341
344
|
export interface GetOptions {
|
|
345
|
+
/** Password to unlock private keys in memory. Defaults to DOTENVX_LOCK_PASSWORD. */
|
|
346
|
+
lockPassword?: string;
|
|
347
|
+
|
|
342
348
|
/**
|
|
343
349
|
* Specify a custom path if your file containing environment variables is located elsewhere.
|
|
344
350
|
* Can also be an array of strings, specifying multiple paths.
|
package/src/lib/main.js
CHANGED
|
@@ -92,6 +92,7 @@ const config = function (options = {}) {
|
|
|
92
92
|
noNative,
|
|
93
93
|
no1Password: options.no1Password,
|
|
94
94
|
noBitwarden: options.noBitwarden,
|
|
95
|
+
lockPassword: options.lockPassword,
|
|
95
96
|
noSpinner: options.noSpinner,
|
|
96
97
|
token: options.token
|
|
97
98
|
})
|
|
@@ -259,12 +260,13 @@ const set = async function (key, value, options = {}) {
|
|
|
259
260
|
noNative,
|
|
260
261
|
no1Password: options.no1Password,
|
|
261
262
|
noBitwarden: options.noBitwarden,
|
|
263
|
+
lockPassword: options.lockPassword,
|
|
262
264
|
noCreate,
|
|
263
265
|
encrypt
|
|
264
266
|
})
|
|
265
267
|
|
|
266
268
|
if (keysSrc) {
|
|
267
|
-
fsx.
|
|
269
|
+
fsx.writeKeyFileSync(envKeysFilepath || '.env.keys', keysSrc)
|
|
268
270
|
}
|
|
269
271
|
|
|
270
272
|
let withEncryption = ''
|
|
@@ -341,7 +343,8 @@ const get = async function (key, options = {}) {
|
|
|
341
343
|
noArmor,
|
|
342
344
|
noNative,
|
|
343
345
|
no1Password: options.no1Password,
|
|
344
|
-
noBitwarden: options.noBitwarden
|
|
346
|
+
noBitwarden: options.noBitwarden,
|
|
347
|
+
lockPassword: options.lockPassword
|
|
345
348
|
})
|
|
346
349
|
|
|
347
350
|
if (options.mask !== undefined) {
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const armorProvider = require('./armor/index')
|
|
4
|
-
const nativeProvider = require('./native/index')
|
|
5
|
-
const onePasswordCustody = require('../helpers/onePasswordCustody')
|
|
6
|
-
const bitwardenCustody = require('../helpers/bitwardenCustody')
|
|
7
|
-
|
|
8
|
-
function syncArmorProvider (publicKeyHex) {
|
|
9
|
-
const { createSyncFn } = require('@dotenvx/tooling')
|
|
10
|
-
const runProviderSync = createSyncFn(require.resolve('./provider-worker.js'))
|
|
11
|
-
return runProviderSync(publicKeyHex)
|
|
12
|
-
}
|
|
1
|
+
const custodians = require('../custodians')
|
|
13
2
|
|
|
14
3
|
function hasKey (keyring, publicKeyHex) {
|
|
15
4
|
return keyring && keyring[publicKeyHex]
|
|
@@ -37,32 +26,6 @@ function composeProvidersSync (providerFns) {
|
|
|
37
26
|
}
|
|
38
27
|
}
|
|
39
28
|
|
|
40
|
-
function armorProviderForOptions (options) {
|
|
41
|
-
return (publicKeyHex) => armorProvider(publicKeyHex, {
|
|
42
|
-
onStatus: options.onStatus,
|
|
43
|
-
token: options.token,
|
|
44
|
-
command: options.command
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function useNative (options) {
|
|
49
|
-
if (!['darwin', 'linux', 'win32'].includes(process.platform)) return false
|
|
50
|
-
if (process.env.CI) return false
|
|
51
|
-
return options.noNative !== true && options.native !== false && process.env.DOTENVX_NO_NATIVE !== 'true'
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function useOnePassword (options) {
|
|
55
|
-
return options.no1Password !== true && process.env.DOTENVX_NO_1PASSWORD !== 'true' && onePasswordCustody.configured()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function useBitwarden (options) {
|
|
59
|
-
return options.noBitwarden !== true && process.env.DOTENVX_NO_BITWARDEN !== 'true' && bitwardenCustody.configured()
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function useArmor (options, noArmor) {
|
|
63
|
-
return options.noArmor !== true && options.armor !== false && !noArmor
|
|
64
|
-
}
|
|
65
|
-
|
|
66
29
|
function providerFrom (providerFns, compose) {
|
|
67
30
|
if (providerFns.length === 0) return null
|
|
68
31
|
if (providerFns.length === 1) return providerFns[0]
|
|
@@ -75,21 +38,11 @@ async function providers (options = {}) {
|
|
|
75
38
|
return options.provider
|
|
76
39
|
}
|
|
77
40
|
|
|
78
|
-
const providerFns =
|
|
79
|
-
|
|
80
|
-
if (useNative(options)) {
|
|
81
|
-
providerFns.push(nativeProvider)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (useOnePassword(options)) providerFns.push(onePasswordCustody.get)
|
|
85
|
-
if (useBitwarden(options)) providerFns.push(bitwardenCustody.get)
|
|
41
|
+
const providerFns = custodians.providers(options)
|
|
86
42
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (useArmor(options, noArmor)) {
|
|
91
|
-
providerFns.push(armorProviderForOptions(options))
|
|
92
|
-
}
|
|
43
|
+
const armor = custodians.get('armored')
|
|
44
|
+
if (armor.enabled(options) && await armor.configured(options)) {
|
|
45
|
+
providerFns.push(publicKey => armor.get(publicKey, options))
|
|
93
46
|
}
|
|
94
47
|
|
|
95
48
|
return providerFrom(providerFns, composeProviders)
|
|
@@ -100,21 +53,11 @@ providers.sync = function providersSync (options = {}) {
|
|
|
100
53
|
return options.provider
|
|
101
54
|
}
|
|
102
55
|
|
|
103
|
-
const providerFns =
|
|
104
|
-
|
|
105
|
-
if (useNative(options)) {
|
|
106
|
-
providerFns.push(nativeProvider)
|
|
107
|
-
}
|
|
56
|
+
const providerFns = custodians.providers(options, true)
|
|
108
57
|
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
if (options.noArmor !== true && options.armor !== false) {
|
|
113
|
-
const sesh = new Session()
|
|
114
|
-
const noArmor = !options.token && sesh.noArmorSync()
|
|
115
|
-
if (useArmor(options, noArmor)) {
|
|
116
|
-
providerFns.push(syncArmorProvider)
|
|
117
|
-
}
|
|
58
|
+
const armor = custodians.get('armored')
|
|
59
|
+
if (armor.enabled(options) && armor.configuredSync(options)) {
|
|
60
|
+
providerFns.push(armor.getSync)
|
|
118
61
|
}
|
|
119
62
|
|
|
120
63
|
return providerFrom(providerFns, composeProvidersSync)
|
|
@@ -1,62 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const linuxSecretService = require('../../helpers/linuxSecretService')
|
|
4
|
-
|
|
5
|
-
function get (key) {
|
|
6
|
-
if (process.platform === 'win32') {
|
|
7
|
-
return windowsCredentialManager.get(key)
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
if (process.platform === 'linux') {
|
|
11
|
-
return linuxSecretService.get(key)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return macosKeychain.get(key)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function set (key, value, label = key) {
|
|
18
|
-
if (process.platform === 'win32') {
|
|
19
|
-
windowsCredentialManager.set(key, value, label)
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (process.platform === 'linux') {
|
|
24
|
-
linuxSecretService.set(key, value, label)
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
macosKeychain.set(key, value, label)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
index.delete = function (key) {
|
|
32
|
-
if (process.platform === 'win32') {
|
|
33
|
-
windowsCredentialManager.delete(key)
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (process.platform === 'linux') {
|
|
38
|
-
linuxSecretService.delete(key)
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
macosKeychain.delete(key)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function index (publicKeyHex) {
|
|
46
|
-
if (!['darwin', 'linux', 'win32'].includes(process.platform)) return {}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const privateKeyHex = get(publicKeyHex)
|
|
50
|
-
|
|
51
|
-
if (!privateKeyHex) return {}
|
|
52
|
-
|
|
53
|
-
return { [publicKeyHex]: privateKeyHex }
|
|
54
|
-
} catch {
|
|
55
|
-
return {}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
index.set = set
|
|
60
|
-
index.get = get
|
|
61
|
-
|
|
62
|
-
module.exports = index
|
|
1
|
+
// Compatibility adapter for session credentials and native transfer commands.
|
|
2
|
+
module.exports = require('../../custodians/local/native/backend')
|
|
@@ -54,8 +54,9 @@ function inject (processEnv, parsed) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function buildParseOptions ({ processEnv, overload, envKeysFilepath, provider, decryptor, proxyCredentials, proxyRules }) {
|
|
57
|
+
function buildParseOptions ({ lockPassword, processEnv, overload, envKeysFilepath, provider, decryptor, proxyCredentials, proxyRules }) {
|
|
58
58
|
const options = {
|
|
59
|
+
lockPassword,
|
|
59
60
|
processEnv,
|
|
60
61
|
overload,
|
|
61
62
|
proxyCredentials,
|
|
@@ -76,7 +77,7 @@ function buildParseOptions ({ processEnv, overload, envKeysFilepath, provider, d
|
|
|
76
77
|
return options
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, no1Password, noBitwarden, onStatus, proxyCredentials, proxyRules }) {
|
|
80
|
+
async function injectEnv ({ env, lockPassword, overload, processEnv, envKeysFilepath, provider, decryptor, no1Password, noBitwarden, onStatus, proxyCredentials, proxyRules }) {
|
|
80
81
|
const row = {}
|
|
81
82
|
row.type = TYPE_ENV
|
|
82
83
|
row.string = env.value
|
|
@@ -90,6 +91,7 @@ async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider
|
|
|
90
91
|
const parseOptions = buildParseOptions({
|
|
91
92
|
processEnv: parseProcessEnv,
|
|
92
93
|
overload,
|
|
94
|
+
lockPassword,
|
|
93
95
|
envKeysFilepath,
|
|
94
96
|
provider,
|
|
95
97
|
decryptor,
|
|
@@ -131,7 +133,7 @@ async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider
|
|
|
131
133
|
return row
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
function injectEnvSync ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, no1Password, noBitwarden }) {
|
|
136
|
+
function injectEnvSync ({ env, lockPassword, overload, processEnv, envKeysFilepath, provider, decryptor, no1Password, noBitwarden }) {
|
|
135
137
|
const row = {}
|
|
136
138
|
row.type = TYPE_ENV
|
|
137
139
|
row.string = env.value
|
|
@@ -145,6 +147,7 @@ function injectEnvSync ({ env, overload, processEnv, envKeysFilepath, provider,
|
|
|
145
147
|
const parseOptions = buildParseOptions({
|
|
146
148
|
processEnv: parseProcessEnv,
|
|
147
149
|
overload,
|
|
150
|
+
lockPassword,
|
|
148
151
|
envKeysFilepath,
|
|
149
152
|
provider,
|
|
150
153
|
decryptor
|
|
@@ -183,7 +186,7 @@ function injectEnvSync ({ env, overload, processEnv, envKeysFilepath, provider,
|
|
|
183
186
|
return row
|
|
184
187
|
}
|
|
185
188
|
|
|
186
|
-
async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, readableFilepaths, no1Password, noBitwarden, onStatus, proxyCredentials, proxyRules }) {
|
|
189
|
+
async function injectEnvFile ({ env, lockPassword, overload, processEnv, envKeysFilepath, provider, decryptor, readableFilepaths, no1Password, noBitwarden, onStatus, proxyCredentials, proxyRules }) {
|
|
187
190
|
const row = {}
|
|
188
191
|
row.type = TYPE_ENV_FILE
|
|
189
192
|
row.filepath = env.value
|
|
@@ -199,6 +202,7 @@ async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, prov
|
|
|
199
202
|
const parseOptions = buildParseOptions({
|
|
200
203
|
processEnv,
|
|
201
204
|
overload,
|
|
205
|
+
lockPassword,
|
|
202
206
|
envKeysFilepath: fk,
|
|
203
207
|
provider,
|
|
204
208
|
decryptor,
|
|
@@ -245,7 +249,7 @@ async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, prov
|
|
|
245
249
|
return row
|
|
246
250
|
}
|
|
247
251
|
|
|
248
|
-
function injectEnvFileSync ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, readableFilepaths, no1Password, noBitwarden }) {
|
|
252
|
+
function injectEnvFileSync ({ env, lockPassword, overload, processEnv, envKeysFilepath, provider, decryptor, readableFilepaths, no1Password, noBitwarden }) {
|
|
249
253
|
const row = {}
|
|
250
254
|
row.type = TYPE_ENV_FILE
|
|
251
255
|
row.filepath = env.value
|
|
@@ -261,6 +265,7 @@ function injectEnvFileSync ({ env, overload, processEnv, envKeysFilepath, provid
|
|
|
261
265
|
const parseOptions = buildParseOptions({
|
|
262
266
|
processEnv,
|
|
263
267
|
overload,
|
|
268
|
+
lockPassword,
|
|
264
269
|
envKeysFilepath: fk,
|
|
265
270
|
provider,
|
|
266
271
|
decryptor
|
|
@@ -318,6 +323,7 @@ async function envs (options = {}) {
|
|
|
318
323
|
processedEnvs.push(await injectEnvFile({
|
|
319
324
|
env,
|
|
320
325
|
overload: options.overload,
|
|
326
|
+
lockPassword: options.lockPassword,
|
|
321
327
|
processEnv,
|
|
322
328
|
envKeysFilepath,
|
|
323
329
|
provider,
|
|
@@ -333,6 +339,7 @@ async function envs (options = {}) {
|
|
|
333
339
|
processedEnvs.push(await injectEnv({
|
|
334
340
|
env,
|
|
335
341
|
overload: options.overload,
|
|
342
|
+
lockPassword: options.lockPassword,
|
|
336
343
|
processEnv,
|
|
337
344
|
envKeysFilepath,
|
|
338
345
|
provider,
|
|
@@ -367,6 +374,7 @@ function envsSync (options = {}) {
|
|
|
367
374
|
processedEnvs.push(injectEnvFileSync({
|
|
368
375
|
env,
|
|
369
376
|
overload: options.overload,
|
|
377
|
+
lockPassword: options.lockPassword,
|
|
370
378
|
processEnv,
|
|
371
379
|
envKeysFilepath,
|
|
372
380
|
provider,
|
|
@@ -379,6 +387,7 @@ function envsSync (options = {}) {
|
|
|
379
387
|
processedEnvs.push(injectEnvSync({
|
|
380
388
|
env,
|
|
381
389
|
overload: options.overload,
|
|
390
|
+
lockPassword: options.lockPassword,
|
|
382
391
|
processEnv,
|
|
383
392
|
envKeysFilepath,
|
|
384
393
|
provider,
|
package/src/lib/resolvers/get.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const matchesStoredKey = require('../helpers/matchesStoredKey')
|
|
2
2
|
const keynames = require('../conventions/keynames')
|
|
3
3
|
const readEnvKey = require('../helpers/readEnvKey')
|
|
4
4
|
const removeEnvKey = require('../helpers/removeEnvKey')
|
|
@@ -6,7 +6,7 @@ const upsertEnvKey = require('../helpers/upsertEnvKey')
|
|
|
6
6
|
|
|
7
7
|
function verify (publicKey, privateKey) {
|
|
8
8
|
try {
|
|
9
|
-
if (
|
|
9
|
+
if (matchesStoredKey(publicKey, privateKey)) return
|
|
10
10
|
} catch {}
|
|
11
11
|
throw new Error('private key does not match the .env public key')
|
|
12
12
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const { scan, encrypted } = require('@dotenvx/primitives')
|
|
4
|
+
|
|
5
|
+
module.exports = function init ({ directory = process.cwd(), envFile } = {}) {
|
|
6
|
+
const target = path.resolve(directory, 'Envfile')
|
|
7
|
+
// lstat also preserves dangling symlinks. Never overwrite an existing Envfile.
|
|
8
|
+
try {
|
|
9
|
+
fs.lstatSync(target)
|
|
10
|
+
return { created: false }
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error.code !== 'ENOENT') throw error
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sources = []
|
|
16
|
+
const names = new Set()
|
|
17
|
+
let requireEncryption = false
|
|
18
|
+
for (const candidate of envFile ? [envFile] : ['.env.example', '.env']) {
|
|
19
|
+
let src
|
|
20
|
+
try {
|
|
21
|
+
src = fs.readFileSync(path.resolve(directory, candidate), 'utf8')
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (envFile || error.code !== 'ENOENT') throw error
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
sources.push(candidate)
|
|
27
|
+
// scan preserves every assignment as an array, without decrypting or expanding.
|
|
28
|
+
for (const [key, values] of Object.entries(scan(src).parsed)) {
|
|
29
|
+
if (/^DOTENV_(?:PUBLIC|PRIVATE)_KEY(?:_|$)/.test(key)) continue
|
|
30
|
+
names.add(key)
|
|
31
|
+
if (values.some(value => encrypted(value))) requireEncryption = true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const keys = [...names]
|
|
36
|
+
const invalid = keys.filter(key => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
37
|
+
if (invalid.length) throw new Error(`Unsupported Envfile variable names: ${invalid.join(', ')}`)
|
|
38
|
+
|
|
39
|
+
const declarations = keys.map(key => `env "${key}"`).join('\n')
|
|
40
|
+
const content = `encrypted ${requireEncryption}\n` +
|
|
41
|
+
(declarations ? `\n${declarations}\n` : '')
|
|
42
|
+
try {
|
|
43
|
+
fs.writeFileSync(target, content, { flag: 'wx' })
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.code === 'EEXIST') return { created: false }
|
|
46
|
+
throw error
|
|
47
|
+
}
|
|
48
|
+
return { created: true, source: sources.join(', '), count: keys.length }
|
|
49
|
+
}
|
|
@@ -17,10 +17,14 @@ class Precommit {
|
|
|
17
17
|
this.directory = directory
|
|
18
18
|
// options
|
|
19
19
|
this.install = options.install
|
|
20
|
+
this.global = options.global
|
|
20
21
|
this.excludeEnvFile = ['test/**', 'tests/**', 'spec/**', 'specs/**', 'pytest/**', 'test_suite/**']
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
run () {
|
|
25
|
+
if (this.global && !this.install) {
|
|
26
|
+
throw new Error('--global requires --install')
|
|
27
|
+
}
|
|
24
28
|
if (this.install) {
|
|
25
29
|
const {
|
|
26
30
|
successMessage
|
|
@@ -136,7 +140,14 @@ class Precommit {
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
_installPrecommitHook () {
|
|
139
|
-
|
|
143
|
+
if (this.global) {
|
|
144
|
+
const attributesPath = require('../helpers/installPrecommitFilter')(true)
|
|
145
|
+
return { successMessage: `▣ dotenvx required clean filter installed globally [${attributesPath}]` }
|
|
146
|
+
}
|
|
147
|
+
const hookPath = require('../helpers/installPrecommitFilter')()
|
|
148
|
+
const result = new InstallPrecommitHook(hookPath).run()
|
|
149
|
+
result.successMessage += '\n▣ dotenvx required clean filter installed (blocks staging plaintext .env files)'
|
|
150
|
+
return result
|
|
140
151
|
}
|
|
141
152
|
}
|
|
142
153
|
|
|
@@ -31,6 +31,7 @@ module.exports = async function validate ({ envs = [], options = {}, processEnv
|
|
|
31
31
|
noNative: options.native === false || options.noNative === true,
|
|
32
32
|
no1Password: options['1password'] === false || options.no1Password === true,
|
|
33
33
|
noBitwarden: options.bitwarden === false || options.noBitwarden === true,
|
|
34
|
+
lockPassword: options.lockPassword,
|
|
34
35
|
token: options.token,
|
|
35
36
|
command,
|
|
36
37
|
onStatus
|
|
@@ -8,18 +8,11 @@ const SAMPLE_ENV_KIT = require('../helpers/kits/sample')
|
|
|
8
8
|
const Errors = require('../helpers/errors')
|
|
9
9
|
const { determine } = require('./../helpers/envResolution')
|
|
10
10
|
const detectEncoding = require('./../helpers/detectEncoding')
|
|
11
|
-
const { isDotenvPublicKey, isPlainKey, mutateSrc
|
|
11
|
+
const { isDotenvPublicKey, isPlainKey, mutateSrc } = require('../helpers/cryptography')
|
|
12
12
|
const keynames = require('../conventions/keynames')
|
|
13
|
-
const PostArmorUp = require('../api/postArmorUp')
|
|
14
|
-
const prompts = require('../helpers/prompts')
|
|
15
|
-
const teamChoicesFromMeta = require('../helpers/teamChoicesFromMeta')
|
|
16
|
-
const isTeamRequiredError = require('../helpers/isTeamRequiredError')
|
|
17
|
-
const Session = require('../../db/session')
|
|
18
13
|
|
|
19
14
|
const selectKeyStorage = require('../helpers/selectKeyStorage')
|
|
20
|
-
const
|
|
21
|
-
const onePasswordCustody = require('../helpers/onePasswordCustody')
|
|
22
|
-
const bitwardenCustody = require('../helpers/bitwardenCustody')
|
|
15
|
+
const custodians = require('../custodians')
|
|
23
16
|
|
|
24
17
|
async function encryptTransform (options = {}) {
|
|
25
18
|
const envs = options.envs || []
|
|
@@ -27,6 +20,7 @@ async function encryptTransform (options = {}) {
|
|
|
27
20
|
const ek = options.ek
|
|
28
21
|
const fk = options.fk || '.env.keys'
|
|
29
22
|
let storage
|
|
23
|
+
const custodyContext = {}
|
|
30
24
|
const noCreate = options.noCreate
|
|
31
25
|
|
|
32
26
|
const processedEnvs = []
|
|
@@ -87,46 +81,9 @@ async function encryptTransform (options = {}) {
|
|
|
87
81
|
|
|
88
82
|
const comment = path.basename(envFilepath)
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
await onePasswordCustody.set(publicKey, privateKey)
|
|
94
|
-
} else if (storage === 'native' && storeNativePrivateKey(publicKey, privateKey, fk)) {
|
|
95
|
-
row.nativePrivateKeyAdded = true
|
|
96
|
-
} else if (storage !== 'armored') {
|
|
97
|
-
const mutated = mutateKeysSrc({ keysSrc, privateKeyName, privateKeyValue: privateKey, comment })
|
|
98
|
-
keysSrc = mutated.keysSrc
|
|
99
|
-
} else {
|
|
100
|
-
const sesh = new Session()
|
|
101
|
-
const hostname = sesh.hostname()
|
|
102
|
-
const token = sesh.token()
|
|
103
|
-
const devicePublicKey = sesh.devicePublicKey()
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
await new PostArmorUp(hostname, token, devicePublicKey, publicKey, privateKey, undefined).run()
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (!isTeamRequiredError(error)) {
|
|
109
|
-
throw error
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const choices = teamChoicesFromMeta(error.meta)
|
|
113
|
-
|
|
114
|
-
let team = choices[0].value
|
|
115
|
-
if (choices.length > 1) {
|
|
116
|
-
team = await prompts.select({
|
|
117
|
-
message: 'Select team',
|
|
118
|
-
choices
|
|
119
|
-
}, {
|
|
120
|
-
input: process.stdin,
|
|
121
|
-
output: process.stderr
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
await new PostArmorUp(hostname, token, devicePublicKey, publicKey, privateKey, team).run()
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// don't set keysSrc (in armor)
|
|
129
|
-
}
|
|
84
|
+
const stored = await custodians.store(storage, publicKey, privateKey, Object.assign(custodyContext, { keysSrc, privateKeyName, comment, keysFilepath: fk }))
|
|
85
|
+
if (Object.prototype.hasOwnProperty.call(stored, 'keysSrc')) keysSrc = stored.keysSrc
|
|
86
|
+
if (stored.nativePrivateKeyAdded) row.nativePrivateKeyAdded = true
|
|
130
87
|
}
|
|
131
88
|
|
|
132
89
|
const { parsed } = scan(row.envSrc, { ik, ek })
|