@dotenvx/dotenvx 2.26.0 → 2.27.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/CHANGELOG.md +14 -2
- package/README.md +48 -40
- package/package.json +12 -2
- package/src/cli/actions/decrypt.js +3 -3
- package/src/cli/actions/encrypt.js +3 -3
- package/src/cli/actions/get.js +2 -2
- package/src/cli/actions/keypair.js +1 -1
- package/src/cli/actions/run.js +46 -42
- package/src/cli/actions/set.js +2 -2
- package/src/cli/actions/validate.js +21 -33
- package/src/cli/commands/armor.js +1 -1
- package/src/cli/commands/custody.js +43 -0
- package/src/cli/commands/native.js +1 -1
- package/src/cli/dotenvx.js +11 -6
- package/src/lib/grammars/envfile.peggy +98 -0
- package/src/lib/helpers/bitwardenCustody.js +9 -1
- package/src/lib/helpers/encryptedSources.js +15 -0
- package/src/lib/helpers/envfileParser.js +2731 -0
- package/src/lib/helpers/errors.js +16 -16
- package/src/lib/helpers/executeCommand.js +6 -2
- package/src/lib/helpers/formatEnvfileSyntaxError.js +24 -0
- package/src/lib/helpers/isValidEmail.js +11 -0
- package/src/lib/helpers/isValidUrl.js +9 -0
- package/src/lib/helpers/onePasswordCustody.js +10 -2
- package/src/lib/helpers/parseWithDecryptor.js +9 -1
- package/src/lib/helpers/readEnvfile.js +128 -0
- package/src/lib/helpers/selectKeyStorage.js +2 -2
- package/src/lib/helpers/validate.js +83 -14
- package/src/lib/helpers/validateEnvfile.js +24 -0
- package/src/lib/main.js +7 -7
- package/src/lib/providers/index.js +1 -1
- package/src/lib/proxy/configureProxy.js +55 -0
- package/src/lib/proxy/eligibleHeaders.js +16 -0
- package/src/lib/proxy/prepareProxy.js +24 -0
- package/src/lib/proxy/proxyCertificates.js +44 -0
- package/src/lib/proxy/proxyForward.js +65 -0
- package/src/lib/proxy/proxyPreload.js +21 -0
- package/src/lib/proxy/proxyPreloadSource.js +3 -0
- package/src/lib/proxy/proxyServer.js +175 -0
- package/src/lib/resolvers/envs.js +15 -5
- package/src/lib/resolvers/get.js +1 -1
- package/src/lib/services/custodyTransfer.js +50 -0
- package/src/lib/services/validate.js +58 -0
- package/src/lib/transforms/set.js +2 -2
- package/src/lib/helpers/validateEnvExample.js +0 -24
|
@@ -14,7 +14,6 @@ const ISSUE_BY_CODE = {
|
|
|
14
14
|
MALFORMED_ENCRYPTED_DATA: 'https://github.com/dotenvx/dotenvx/issues/467',
|
|
15
15
|
MISPAIRED_PRIVATE_KEY: 'https://github.com/dotenvx/dotenvx/issues/752',
|
|
16
16
|
MISSING_DIRECTORY: 'https://github.com/dotenvx/dotenvx/issues/758',
|
|
17
|
-
MISSING_ENV_EXAMPLE: 'https://github.com/dotenvx/dotenvx/issues/905',
|
|
18
17
|
MISSING_ENV_FILE: 'https://github.com/dotenvx/dotenvx/issues/484',
|
|
19
18
|
MISSING_ENV_KEYS_FILE: 'https://github.com/dotenvx/dotenvx/issues/775',
|
|
20
19
|
MISSING_ENV_FILES: 'https://github.com/dotenvx/dotenvx/issues/760',
|
|
@@ -25,7 +24,6 @@ const ISSUE_BY_CODE = {
|
|
|
25
24
|
MISSING_VALUE: 'https://github.com/dotenvx/dotenvx/issues/864',
|
|
26
25
|
FILE_NOT_WRITABLE: 'https://github.com/dotenvx/dotenvx/issues/890',
|
|
27
26
|
PRECOMMIT_HOOK_MODIFY_FAILED: 'try again or report error',
|
|
28
|
-
VALIDATION_FAILED: 'https://github.com/dotenvx/dotenvx/issues/907',
|
|
29
27
|
WRONG_PRIVATE_KEY: 'https://github.com/dotenvx/dotenvx/issues/466'
|
|
30
28
|
}
|
|
31
29
|
|
|
@@ -235,15 +233,10 @@ class Errors {
|
|
|
235
233
|
return e
|
|
236
234
|
}
|
|
237
235
|
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const e = new Error(message)
|
|
244
|
-
e.code = code
|
|
245
|
-
e.help = help
|
|
246
|
-
e.messageWithHelp = `${message}. ${help}`
|
|
236
|
+
envfileRequired () {
|
|
237
|
+
const e = new Error('[ENVFILE_REQUIRED] validate requires an Envfile')
|
|
238
|
+
e.code = 'ENVFILE_REQUIRED'
|
|
239
|
+
e.messageWithHelp = e.message
|
|
247
240
|
return e
|
|
248
241
|
}
|
|
249
242
|
|
|
@@ -345,15 +338,22 @@ class Errors {
|
|
|
345
338
|
return e
|
|
346
339
|
}
|
|
347
340
|
|
|
348
|
-
|
|
349
|
-
const code = '
|
|
341
|
+
malformedEnvfile () {
|
|
342
|
+
const code = 'MALFORMED_ENVFILE'
|
|
343
|
+
const message = `[${code}] ${this.message}`
|
|
344
|
+
const e = new Error(message)
|
|
345
|
+
e.code = code
|
|
346
|
+
e.messageWithHelp = message
|
|
347
|
+
return e
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
invalidEnv () {
|
|
351
|
+
const code = 'INVALID_ENV'
|
|
350
352
|
const message = `[${code}] ${this.message}`
|
|
351
|
-
const help = `fix: [${ISSUE_BY_CODE[code]}]`
|
|
352
353
|
|
|
353
354
|
const e = new Error(message)
|
|
354
355
|
e.code = code
|
|
355
|
-
e.
|
|
356
|
-
e.messageWithHelp = `${message}. ${help}`
|
|
356
|
+
e.messageWithHelp = message
|
|
357
357
|
return e
|
|
358
358
|
}
|
|
359
359
|
|
|
@@ -6,7 +6,7 @@ const Errors = require('./errors')
|
|
|
6
6
|
const { createRedactedStreamWriter, redactOutput } = require('./redactOutput')
|
|
7
7
|
const ptyCommand = require('./ptyCommand')
|
|
8
8
|
|
|
9
|
-
async function executeCommand (commandArgs, env, sensitiveValues = []) {
|
|
9
|
+
async function executeCommand (commandArgs, env, sensitiveValues = [], onComplete) {
|
|
10
10
|
const FORWARD_SIGNAL_GRACE_MS = 1000
|
|
11
11
|
const FORCE_KILL_GRACE_MS = 1000
|
|
12
12
|
const signals = [
|
|
@@ -17,6 +17,7 @@ async function executeCommand (commandArgs, env, sensitiveValues = []) {
|
|
|
17
17
|
logger.debug(`executing process command [${commandArgs.join(' ')}]`)
|
|
18
18
|
|
|
19
19
|
let child
|
|
20
|
+
let commandExitCode
|
|
20
21
|
let signalSent
|
|
21
22
|
let sigintCount = 0
|
|
22
23
|
const signalForwardTimers = new Set()
|
|
@@ -175,7 +176,7 @@ async function executeCommand (commandArgs, env, sensitiveValues = []) {
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
// Exit with the error code from the command process, or 1 if unavailable
|
|
178
|
-
|
|
179
|
+
commandExitCode = error.exitCode || 1
|
|
179
180
|
} finally {
|
|
180
181
|
signalForwardTimers.forEach(timer => clearTimeout(timer))
|
|
181
182
|
signalForwardTimers.clear()
|
|
@@ -188,7 +189,10 @@ async function executeCommand (commandArgs, env, sensitiveValues = []) {
|
|
|
188
189
|
otherSignalHandlers.forEach((handler, signal) => {
|
|
189
190
|
process.removeListener(signal, handler)
|
|
190
191
|
})
|
|
192
|
+
if (onComplete) await onComplete()
|
|
191
193
|
}
|
|
194
|
+
|
|
195
|
+
if (commandExitCode) process.exit(commandExitCode)
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
module.exports = executeCommand
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const path = require('node:path')
|
|
2
|
+
|
|
3
|
+
module.exports = function formatEnvfileSyntaxError (error, src, filepath) {
|
|
4
|
+
const location = error.location?.start
|
|
5
|
+
if (!location) return error.message
|
|
6
|
+
|
|
7
|
+
const filename = path.relative(process.cwd(), filepath) || path.basename(filepath)
|
|
8
|
+
const line = src.split(/\r\n|\n|\r/)[location.line - 1] || ''
|
|
9
|
+
let message = error.message
|
|
10
|
+
if (error.expected) {
|
|
11
|
+
const remaining = src.slice(location.offset)
|
|
12
|
+
const token = remaining.match(/^(?:"[^"\r\n]*"|'[^'\r\n]*'|[A-Za-z0-9_]+|[^\r\n])/u)?.[0]
|
|
13
|
+
const displayToken = token && /^["']/.test(token) ? token : JSON.stringify(token)
|
|
14
|
+
message = token ? `Unexpected ${displayToken}` : 'Unexpected end of input'
|
|
15
|
+
const expected = [...new Set(error.expected.flatMap(item => {
|
|
16
|
+
if (item.type === 'other') return [item.description]
|
|
17
|
+
if (item.type === 'literal' && item.text.trim() && item.text !== '#') return [JSON.stringify(item.text)]
|
|
18
|
+
return []
|
|
19
|
+
}))]
|
|
20
|
+
if (expected.length > 0 && expected.length <= 3) message += `; expected ${expected.join(' or ')}`
|
|
21
|
+
}
|
|
22
|
+
const prefix = `${location.line} | `
|
|
23
|
+
return `${filename}:${location.line}:${location.column}: ${message}\n${prefix}${line}\n${' '.repeat(prefix.length)}${line.slice(0, location.column - 1).replace(/[^\t]/g, ' ')}^`
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module.exports = function isValidEmail (value) {
|
|
2
|
+
if (value.length > 254 || /\s/.test(value)) return false
|
|
3
|
+
const parts = value.split('@')
|
|
4
|
+
if (parts.length !== 2) return false
|
|
5
|
+
const [local, domain] = parts
|
|
6
|
+
if (local.length > 64 || !/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*$/i.test(local)) return false
|
|
7
|
+
const labels = domain.split('.')
|
|
8
|
+
return labels.length > 1 &&
|
|
9
|
+
labels.every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label)) &&
|
|
10
|
+
/^(?:[a-z]{2,63}|xn--[a-z0-9-]+)$/i.test(labels[labels.length - 1])
|
|
11
|
+
}
|
|
@@ -15,7 +15,7 @@ function failure (message) {
|
|
|
15
15
|
|
|
16
16
|
function commandError (args, stderr) {
|
|
17
17
|
const step = args[0] === 'item'
|
|
18
|
-
? 'create the key item'
|
|
18
|
+
? (args[1] === 'delete' ? 'delete the key item' : 'create the key item')
|
|
19
19
|
: {
|
|
20
20
|
whoami: 'check the signed-in account',
|
|
21
21
|
signin: 'sign in',
|
|
@@ -156,4 +156,12 @@ async function set (publicKey, privateKey) {
|
|
|
156
156
|
new Session().createStore().set(`${PREFIX}${publicKey}`, `${account}|${reference}`)
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
async function remove (publicKey) {
|
|
160
|
+
const loc = location(publicKey)
|
|
161
|
+
if (!loc) return
|
|
162
|
+
const [vault, item] = loc.reference.slice('op://'.length).split('/')
|
|
163
|
+
await run(['item', 'delete', item, `--vault=${vault}`, `--account=${loc.account}`])
|
|
164
|
+
new Session().openStore().delete(`${PREFIX}${publicKey}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { available, configured, get, getSync, set, delete: remove }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const { parse, parseSync, parsearrays } = require('@dotenvx/primitives')
|
|
1
|
+
const { parse, parseSync, parsearrays, scan, encrypted } = require('@dotenvx/primitives')
|
|
2
|
+
const prepareProxy = require('../proxy/prepareProxy')
|
|
2
3
|
const SERVER_SIDE_DECRYPTION_REQUIRED = 'SERVER_SIDE_DECRYPTION_REQUIRED'
|
|
3
4
|
|
|
4
5
|
function decryptOptions (error) {
|
|
@@ -27,6 +28,13 @@ function failedKeyAccessFallback (result, error) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
async function parseWithDecryptor (src, options = {}) {
|
|
31
|
+
if (options.proxyCredentials) {
|
|
32
|
+
const original = src
|
|
33
|
+
src = prepareProxy(src, options.proxyCredentials, options.processEnv, options.proxyRules)
|
|
34
|
+
if (src !== original && !Object.values(scan(src).parsed).flat().some(encrypted)) {
|
|
35
|
+
options = parseOptionsWithoutProvider(options)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
30
38
|
return parseWith(src, options, parse)
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const { isIP } = require('node:net')
|
|
4
|
+
const parser = require('./envfileParser')
|
|
5
|
+
const Errors = require('./errors')
|
|
6
|
+
const isValidUrl = require('./isValidUrl')
|
|
7
|
+
const isValidEmail = require('./isValidEmail')
|
|
8
|
+
const formatEnvfileSyntaxError = require('./formatEnvfileSyntaxError')
|
|
9
|
+
|
|
10
|
+
function compileDeclarations (declarations) {
|
|
11
|
+
const proxyRules = new Map()
|
|
12
|
+
const requiredKeys = []
|
|
13
|
+
const types = new Map()
|
|
14
|
+
const enums = new Map()
|
|
15
|
+
const ranges = new Map()
|
|
16
|
+
const encryptedKeys = []
|
|
17
|
+
const names = new Set()
|
|
18
|
+
for (const input of declarations) {
|
|
19
|
+
const declaration = { ...input }
|
|
20
|
+
if (names.has(declaration.name)) throw new Errors({ message: `Duplicate Envfile declaration: ${declaration.name}` }).malformedEnvfile()
|
|
21
|
+
names.add(declaration.name)
|
|
22
|
+
if (declaration.encrypted) encryptedKeys.push(declaration.name)
|
|
23
|
+
if (declaration.type === 'port') {
|
|
24
|
+
declaration.type = 'integer'
|
|
25
|
+
if (declaration.min === undefined || BigInt(declaration.min) < 0n) declaration.min = '0'
|
|
26
|
+
if (declaration.max === undefined || BigInt(declaration.max) > 65535n) declaration.max = '65535'
|
|
27
|
+
}
|
|
28
|
+
if (declaration.required) requiredKeys.push(declaration.name)
|
|
29
|
+
if (declaration.type) types.set(declaration.name, declaration.type)
|
|
30
|
+
if (declaration.min !== undefined || declaration.max !== undefined) {
|
|
31
|
+
if (declaration.type !== 'integer') {
|
|
32
|
+
throw new Errors({ message: `Invalid Envfile range for ${declaration.name}: min and max require type: "integer" or "port".` }).malformedEnvfile()
|
|
33
|
+
}
|
|
34
|
+
if (declaration.min !== undefined && declaration.max !== undefined && BigInt(declaration.min) > BigInt(declaration.max)) {
|
|
35
|
+
throw new Errors({ message: `Invalid Envfile range for ${declaration.name}: min must be less than or equal to max.` }).malformedEnvfile()
|
|
36
|
+
}
|
|
37
|
+
ranges.set(declaration.name, { min: declaration.min, max: declaration.max })
|
|
38
|
+
}
|
|
39
|
+
if (declaration.enum) {
|
|
40
|
+
if (declaration.type === 'integer' && declaration.enum.some(value => !/^[+-]?\d+$/.test(value.trim()))) {
|
|
41
|
+
throw new Errors({ message: `Invalid Envfile enum for ${declaration.name}: expected integer choices.` }).malformedEnvfile()
|
|
42
|
+
}
|
|
43
|
+
if (declaration.type === 'boolean' && declaration.enum.some(value => !['true', 'false', '1', '0'].includes(value))) {
|
|
44
|
+
throw new Errors({ message: `Invalid Envfile enum for ${declaration.name}: expected true, false, 1, or 0 choices.` }).malformedEnvfile()
|
|
45
|
+
}
|
|
46
|
+
if (declaration.type === 'url' && declaration.enum.some(value => !isValidUrl(value))) {
|
|
47
|
+
throw new Errors({ message: `Invalid Envfile enum for ${declaration.name}: expected URL choices.` }).malformedEnvfile()
|
|
48
|
+
}
|
|
49
|
+
if (declaration.type === 'email' && declaration.enum.some(value => !isValidEmail(value))) {
|
|
50
|
+
throw new Errors({ message: `Invalid Envfile enum for ${declaration.name}: expected email choices.` }).malformedEnvfile()
|
|
51
|
+
}
|
|
52
|
+
if (declaration.type === 'ip' && declaration.enum.some(value => isIP(value) === 0)) {
|
|
53
|
+
throw new Errors({ message: `Invalid Envfile enum for ${declaration.name}: expected IPv4 or IPv6 choices.` }).malformedEnvfile()
|
|
54
|
+
}
|
|
55
|
+
enums.set(declaration.name, declaration.enum)
|
|
56
|
+
}
|
|
57
|
+
if (declaration.proxy) {
|
|
58
|
+
const host = declaration.proxy.domain.toLowerCase()
|
|
59
|
+
if (host.length > 253 || isIP(host) || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host)) {
|
|
60
|
+
throw new Errors({ message: `Invalid Envfile proxy host for ${declaration.name}: expected a DNS hostname without a scheme, port, path or wildcard.` }).malformedEnvfile()
|
|
61
|
+
}
|
|
62
|
+
proxyRules.set(declaration.name, host)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { exists: true, proxyRules, requiredKeys, types, enums, ranges, encryptedKeys }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = function readEnvfile (filepath = path.resolve('Envfile'), envFiles = ['.env']) {
|
|
69
|
+
let src
|
|
70
|
+
try {
|
|
71
|
+
src = fs.readFileSync(filepath, 'utf8')
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code === 'ENOENT') return { ...compileDeclarations([]), exists: false }
|
|
74
|
+
throw error
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let document
|
|
78
|
+
try {
|
|
79
|
+
document = parser.parse(src)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Errors({ message: formatEnvfileSyntaxError(error, src, filepath) }).malformedEnvfile()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const defaults = { proxy: false, required: true, encrypted: document.encrypted }
|
|
85
|
+
const base = document.declarations.map(item => ({ ...defaults, ...item }))
|
|
86
|
+
const baseSchema = compileDeclarations(base)
|
|
87
|
+
const selected = new Set(envFiles.map(file => path.resolve(file)))
|
|
88
|
+
const seen = new Set()
|
|
89
|
+
const active = []
|
|
90
|
+
for (const block of document.files) {
|
|
91
|
+
if (/[*?[\]]/.test(block.file)) {
|
|
92
|
+
throw new Errors({ message: `File blocks require an exact filename: ${block.file}` }).malformedEnvfile()
|
|
93
|
+
}
|
|
94
|
+
const file = path.resolve(path.dirname(filepath), block.file)
|
|
95
|
+
if (seen.has(file)) throw new Errors({ message: `Duplicate Envfile file block: ${block.file}` }).malformedEnvfile()
|
|
96
|
+
seen.add(file)
|
|
97
|
+
const merged = new Map(base.map(item => [item.name, { ...item }]))
|
|
98
|
+
if (block.encrypted !== null && block.encrypted !== undefined) {
|
|
99
|
+
for (const item of merged.values()) item.encrypted = block.encrypted
|
|
100
|
+
}
|
|
101
|
+
const names = new Set()
|
|
102
|
+
for (const item of block.declarations) {
|
|
103
|
+
if (names.has(item.name)) throw new Errors({ message: `Duplicate Envfile declaration in ${block.file}: ${item.name}` }).malformedEnvfile()
|
|
104
|
+
names.add(item.name)
|
|
105
|
+
merged.set(item.name, {
|
|
106
|
+
...defaults,
|
|
107
|
+
...(block.encrypted === null || block.encrypted === undefined ? {} : { encrypted: block.encrypted }),
|
|
108
|
+
...merged.get(item.name),
|
|
109
|
+
...item
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
const schema = compileDeclarations([...merged.values()])
|
|
113
|
+
if (selected.has(file)) active.push(schema)
|
|
114
|
+
}
|
|
115
|
+
if (active.length === 0) return baseSchema
|
|
116
|
+
|
|
117
|
+
// Each selected file policy must hold, regardless of file loading order.
|
|
118
|
+
const proxyRules = new Map()
|
|
119
|
+
for (const schema of active) {
|
|
120
|
+
for (const [key, host] of schema.proxyRules) {
|
|
121
|
+
if (proxyRules.has(key) && proxyRules.get(key) !== host) {
|
|
122
|
+
throw new Errors({ message: `Conflicting Envfile proxy domains for ${key} in selected file blocks` }).malformedEnvfile()
|
|
123
|
+
}
|
|
124
|
+
proxyRules.set(key, host)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { ...active[0], proxyRules, schemas: active }
|
|
128
|
+
}
|
|
@@ -9,12 +9,12 @@ const secretStoreNames = {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
async function selectKeyStorage (options = {}) {
|
|
12
|
-
const useNative = !options.
|
|
12
|
+
const useNative = !options.noNative && process.env.DOTENVX_NO_NATIVE !== 'true' && !process.env.CI && ['darwin', 'linux', 'win32'].includes(process.platform)
|
|
13
13
|
const defaultStorage = useNative ? 'native' : 'file'
|
|
14
14
|
if (process.env.CI || options.noCreate || !process.stdin.isTTY || !process.stderr.isTTY) return defaultStorage
|
|
15
15
|
|
|
16
16
|
const choices = [
|
|
17
|
-
...(useNative ? [{ name: `□ Local Custody (${secretStoreNames[process.platform]})`, value: 'native' }] : [])
|
|
17
|
+
...(useNative ? [{ name: `□ Local Custody (Native ${secretStoreNames[process.platform]})`, value: 'native' }] : [])
|
|
18
18
|
]
|
|
19
19
|
if (!options.no1Password && process.env.DOTENVX_NO_1PASSWORD !== 'true' && await onePasswordCustody.available()) {
|
|
20
20
|
choices.push({ name: '□ Local Custody (1Password)', value: 'onepassword' })
|
|
@@ -1,24 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
for (const [key, values] of Object.entries(comments)) {
|
|
5
|
-
if (values.some(comment => comment && /\boptional\b/i.test(comment))) {
|
|
6
|
-
keys.add(key)
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
return keys
|
|
11
|
-
}
|
|
1
|
+
const isValidUrl = require('./isValidUrl')
|
|
2
|
+
const isValidEmail = require('./isValidEmail')
|
|
3
|
+
const { isIP } = require('node:net')
|
|
12
4
|
|
|
13
5
|
function validate (example = {}, env = {}, options = {}) {
|
|
14
6
|
const errors = []
|
|
15
7
|
const missingRequired = []
|
|
16
|
-
const optional = optionalKeys(options.comments || {})
|
|
17
8
|
|
|
18
9
|
for (const key of Object.keys(example)) {
|
|
19
10
|
const value = env[key]
|
|
20
11
|
const missing = !Object.prototype.hasOwnProperty.call(env, key) || value.trim() === ''
|
|
21
|
-
if (
|
|
12
|
+
if (missing) {
|
|
22
13
|
missingRequired.push(key)
|
|
23
14
|
}
|
|
24
15
|
}
|
|
@@ -27,10 +18,88 @@ function validate (example = {}, env = {}, options = {}) {
|
|
|
27
18
|
errors.push({
|
|
28
19
|
code: 'MISSING_REQUIRED',
|
|
29
20
|
keys: missingRequired,
|
|
30
|
-
message:
|
|
21
|
+
message: missingRequired.map(key => `${key} is required`).join('; ')
|
|
31
22
|
})
|
|
32
23
|
}
|
|
33
24
|
|
|
25
|
+
const invalidIntegers = []
|
|
26
|
+
const invalidBooleans = []
|
|
27
|
+
const invalidUrls = []
|
|
28
|
+
const invalidEmails = []
|
|
29
|
+
const invalidIps = []
|
|
30
|
+
for (const [key, type] of options.types || []) {
|
|
31
|
+
if (!Object.prototype.hasOwnProperty.call(env, key)) continue
|
|
32
|
+
const value = env[key].trim()
|
|
33
|
+
if (value === '') continue // required checks handle blank values
|
|
34
|
+
if (type === 'integer' && !/^[+-]?\d+$/.test(value)) invalidIntegers.push(key)
|
|
35
|
+
if (type === 'boolean' && !['true', 'false', '1', '0'].includes(env[key])) invalidBooleans.push(key)
|
|
36
|
+
if (type === 'url' && !isValidUrl(env[key])) invalidUrls.push(key)
|
|
37
|
+
if (type === 'email' && !isValidEmail(env[key])) invalidEmails.push(key)
|
|
38
|
+
if (type === 'ip' && isIP(env[key]) === 0) invalidIps.push(key)
|
|
39
|
+
}
|
|
40
|
+
if (invalidIntegers.length > 0) {
|
|
41
|
+
errors.push({
|
|
42
|
+
code: 'INVALID_INTEGER',
|
|
43
|
+
keys: invalidIntegers,
|
|
44
|
+
message: invalidIntegers.map(key => `${key} must be an integer`).join('; ')
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (invalidBooleans.length > 0) {
|
|
49
|
+
errors.push({
|
|
50
|
+
code: 'INVALID_BOOLEAN',
|
|
51
|
+
keys: invalidBooleans,
|
|
52
|
+
message: invalidBooleans.map(key => `${key} must be true, false, 1, or 0`).join('; ')
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
if (invalidUrls.length > 0) {
|
|
56
|
+
errors.push({ code: 'INVALID_URL', keys: invalidUrls, message: invalidUrls.map(key => `${key} must be a valid URL`).join('; ') })
|
|
57
|
+
}
|
|
58
|
+
if (invalidEmails.length > 0) {
|
|
59
|
+
errors.push({ code: 'INVALID_EMAIL', keys: invalidEmails, message: invalidEmails.map(key => `${key} must be a valid email address`).join('; ') })
|
|
60
|
+
}
|
|
61
|
+
if (invalidIps.length > 0) {
|
|
62
|
+
errors.push({ code: 'INVALID_IP', keys: invalidIps, message: invalidIps.map(key => `${key} must be a valid IPv4 or IPv6 address`).join('; ') })
|
|
63
|
+
}
|
|
64
|
+
const invalidEnums = []
|
|
65
|
+
for (const [key, choices] of options.enums || []) {
|
|
66
|
+
if (!Object.prototype.hasOwnProperty.call(env, key) || env[key].trim() === '') continue
|
|
67
|
+
if (invalidIntegers.includes(key) || invalidBooleans.includes(key) || invalidUrls.includes(key) || invalidEmails.includes(key) || invalidIps.includes(key)) continue
|
|
68
|
+
const value = env[key]
|
|
69
|
+
const matches = options.types?.get(key) === 'integer'
|
|
70
|
+
? choices.some(choice => BigInt(choice.trim()) === BigInt(value.trim()))
|
|
71
|
+
: choices.includes(value)
|
|
72
|
+
if (!matches) invalidEnums.push(key)
|
|
73
|
+
}
|
|
74
|
+
if (invalidEnums.length > 0) {
|
|
75
|
+
errors.push({
|
|
76
|
+
code: 'INVALID_ENUM',
|
|
77
|
+
keys: invalidEnums,
|
|
78
|
+
message: invalidEnums.map(key => `${key} is not an allowed value`).join('; ')
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const [key, { min, max }] of options.ranges || []) {
|
|
83
|
+
if (!Object.prototype.hasOwnProperty.call(env, key) || env[key].trim() === '') continue
|
|
84
|
+
if (invalidIntegers.includes(key)) continue
|
|
85
|
+
const value = BigInt(env[key].trim())
|
|
86
|
+
if (min !== undefined && value < BigInt(min)) {
|
|
87
|
+
errors.push({ code: 'BELOW_MIN', keys: [key], message: `${key} must be at least ${min}` })
|
|
88
|
+
}
|
|
89
|
+
if (max !== undefined && value > BigInt(max)) {
|
|
90
|
+
errors.push({ code: 'ABOVE_MAX', keys: [key], message: `${key} must be at most ${max}` })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const unencrypted = []
|
|
95
|
+
for (const key of options.encryptedKeys || []) {
|
|
96
|
+
if (!Object.prototype.hasOwnProperty.call(env, key) || env[key].trim() === '') continue
|
|
97
|
+
if (!options.encryptedSources?.has(key)) unencrypted.push(key)
|
|
98
|
+
}
|
|
99
|
+
if (unencrypted.length > 0) {
|
|
100
|
+
errors.push({ code: 'EXPECTED_ENCRYPTED', keys: unencrypted, message: unencrypted.map(key => `${key} is not encrypted`).join('; ') })
|
|
101
|
+
}
|
|
102
|
+
|
|
34
103
|
return {
|
|
35
104
|
valid: errors.length === 0,
|
|
36
105
|
errors
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const validate = require('./validate')
|
|
2
|
+
const encryptedSources = require('./encryptedSources')
|
|
3
|
+
const Errors = require('./errors')
|
|
4
|
+
|
|
5
|
+
module.exports = function validateEnvfile (schema, env, processedEnvs) {
|
|
6
|
+
const messages = new Set()
|
|
7
|
+
const schemas = schema.schemas || [schema]
|
|
8
|
+
const sources = schemas.some(rules => rules.encryptedKeys.length > 0) ? encryptedSources(processedEnvs) : undefined
|
|
9
|
+
for (const rules of schemas) {
|
|
10
|
+
const { requiredKeys, types, enums, ranges, encryptedKeys } = rules
|
|
11
|
+
const required = Object.fromEntries(requiredKeys.map(key => [key, '']))
|
|
12
|
+
const validation = validate(required, env, {
|
|
13
|
+
types,
|
|
14
|
+
enums,
|
|
15
|
+
ranges,
|
|
16
|
+
encryptedKeys,
|
|
17
|
+
encryptedSources: sources
|
|
18
|
+
})
|
|
19
|
+
for (const error of validation.errors) messages.add(error.message)
|
|
20
|
+
}
|
|
21
|
+
if (messages.size > 0) {
|
|
22
|
+
return new Errors({ message: [...messages].join('; ') }).invalidEnv()
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/lib/main.js
CHANGED
|
@@ -73,7 +73,7 @@ const config = function (options = {}) {
|
|
|
73
73
|
|
|
74
74
|
// dotenvx-armor related
|
|
75
75
|
const noArmor = resolveNoArmor(options)
|
|
76
|
-
const
|
|
76
|
+
const noNative = resolveNoNative(options)
|
|
77
77
|
|
|
78
78
|
try {
|
|
79
79
|
let envs = normalizeDotenvConfigPath(buildConfigEnvs(options))
|
|
@@ -89,7 +89,7 @@ const config = function (options = {}) {
|
|
|
89
89
|
processEnv,
|
|
90
90
|
envKeysFile,
|
|
91
91
|
noArmor,
|
|
92
|
-
|
|
92
|
+
noNative,
|
|
93
93
|
no1Password: options.no1Password,
|
|
94
94
|
noBitwarden: options.noBitwarden,
|
|
95
95
|
noSpinner: options.noSpinner,
|
|
@@ -243,7 +243,7 @@ const set = async function (key, value, options = {}) {
|
|
|
243
243
|
const envKeysFilepath = options.envKeysFile
|
|
244
244
|
const noCreate = options.create === false
|
|
245
245
|
const noArmor = resolveNoArmor(options)
|
|
246
|
-
const
|
|
246
|
+
const noNative = resolveNoNative(options)
|
|
247
247
|
|
|
248
248
|
const {
|
|
249
249
|
keysSrc,
|
|
@@ -256,7 +256,7 @@ const set = async function (key, value, options = {}) {
|
|
|
256
256
|
value,
|
|
257
257
|
fk: envKeysFilepath,
|
|
258
258
|
noArmor,
|
|
259
|
-
|
|
259
|
+
noNative,
|
|
260
260
|
no1Password: options.no1Password,
|
|
261
261
|
noBitwarden: options.noBitwarden,
|
|
262
262
|
noCreate,
|
|
@@ -327,7 +327,7 @@ const get = async function (key, options = {}) {
|
|
|
327
327
|
|
|
328
328
|
const envs = buildEnvs(options)
|
|
329
329
|
const noArmor = resolveNoArmor(options)
|
|
330
|
-
const
|
|
330
|
+
const noNative = resolveNoNative(options)
|
|
331
331
|
|
|
332
332
|
// ignore
|
|
333
333
|
const ignore = options.ignore || []
|
|
@@ -339,7 +339,7 @@ const get = async function (key, options = {}) {
|
|
|
339
339
|
all: options.all,
|
|
340
340
|
envKeysFile: options.envKeysFile,
|
|
341
341
|
noArmor,
|
|
342
|
-
|
|
342
|
+
noNative,
|
|
343
343
|
no1Password: options.no1Password,
|
|
344
344
|
noBitwarden: options.noBitwarden
|
|
345
345
|
})
|
|
@@ -410,7 +410,7 @@ function resolveNoArmor (options = {}) {
|
|
|
410
410
|
return options.noArmor === true || (!options.token && sesh.noArmorSync())
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
function
|
|
413
|
+
function resolveNoNative (options = {}) {
|
|
414
414
|
return options.noNative === true || options.native === false
|
|
415
415
|
}
|
|
416
416
|
|
|
@@ -48,7 +48,7 @@ function armorProviderForOptions (options) {
|
|
|
48
48
|
function useNative (options) {
|
|
49
49
|
if (!['darwin', 'linux', 'win32'].includes(process.platform)) return false
|
|
50
50
|
if (process.env.CI) return false
|
|
51
|
-
return options.noNative !== true && options.native !== false &&
|
|
51
|
+
return options.noNative !== true && options.native !== false && process.env.DOTENVX_NO_NATIVE !== 'true'
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function useOnePassword (options) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const path = require('node:path')
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const startProxy = require('./proxyServer')
|
|
4
|
+
|
|
5
|
+
module.exports = async function configureProxy (commandArgs, env, credentials = [], session, explicitToken) {
|
|
6
|
+
const active = credentials.filter(credential => env[credential.name] === credential.placeholder)
|
|
7
|
+
if (active.length === 0) return { commandArgs, env }
|
|
8
|
+
const token = explicitToken || session.token()
|
|
9
|
+
if (!token || token.startsWith('encrypted:')) throw new Error('Credential proxy requires Armor login. Run [dotenvx armor login].')
|
|
10
|
+
const hostname = session.hostname()
|
|
11
|
+
const url = new URL(hostname)
|
|
12
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
|
|
13
|
+
throw new Error('Credential proxy requires an HTTPS Armor hostname.')
|
|
14
|
+
}
|
|
15
|
+
const server = await startProxy({ credentials: active, token, hostname: url.href.replace(/\/$/, ''), devicePublicKey: session.devicePublicKey(), env })
|
|
16
|
+
const preload = path.join(path.dirname(server.caPath), 'proxy-preload.cjs')
|
|
17
|
+
try {
|
|
18
|
+
fs.writeFileSync(preload, require('./proxyPreloadSource'), { mode: 0o600 })
|
|
19
|
+
} catch (error) {
|
|
20
|
+
await server.close()
|
|
21
|
+
throw error
|
|
22
|
+
}
|
|
23
|
+
const nodeOptions = `${env.NODE_OPTIONS || ''} --require ${JSON.stringify(preload)}`.trim()
|
|
24
|
+
const args = /\.(mjs|cjs|js)$/.test(commandArgs[0])
|
|
25
|
+
? [process.pkg ? 'node' : process.execPath, path.resolve(commandArgs[0]), ...commandArgs.slice(1)]
|
|
26
|
+
: commandArgs
|
|
27
|
+
return {
|
|
28
|
+
commandArgs: args,
|
|
29
|
+
close: server.close,
|
|
30
|
+
env: {
|
|
31
|
+
...env,
|
|
32
|
+
NODE_OPTIONS: nodeOptions,
|
|
33
|
+
HTTP_PROXY: server.proxyUrl,
|
|
34
|
+
HTTPS_PROXY: server.proxyUrl,
|
|
35
|
+
ALL_PROXY: server.proxyUrl,
|
|
36
|
+
http_proxy: server.proxyUrl,
|
|
37
|
+
https_proxy: server.proxyUrl,
|
|
38
|
+
all_proxy: server.proxyUrl,
|
|
39
|
+
NO_PROXY: 'localhost,127.0.0.1,::1',
|
|
40
|
+
no_proxy: 'localhost,127.0.0.1,::1',
|
|
41
|
+
NODE_EXTRA_CA_CERTS: server.caPath,
|
|
42
|
+
SSL_CERT_FILE: server.caPath,
|
|
43
|
+
REQUESTS_CA_BUNDLE: server.caPath,
|
|
44
|
+
CURL_CA_BUNDLE: server.caPath,
|
|
45
|
+
GIT_SSL_CAINFO: server.caPath,
|
|
46
|
+
CARGO_HTTP_CAINFO: server.caPath,
|
|
47
|
+
DENO_CERT: server.caPath,
|
|
48
|
+
DOTENVX_PROXY_URL: server.proxyUrl,
|
|
49
|
+
DOTENVX_PROXY_SOCKET: undefined,
|
|
50
|
+
DOTENVX_PROXY_CONFIG: undefined,
|
|
51
|
+
DOTENVX_TOKEN: undefined,
|
|
52
|
+
DOTENVX_ARMOR_TOKEN: undefined
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Keep this policy in sync with Radar's GatewayService::EligibleHeaders.
|
|
2
|
+
// Authentication headers (Authorization, X-Api-Key, etc.) are eligible. Routing,
|
|
3
|
+
// transport, browser context, cookies and proxy control metadata are not.
|
|
4
|
+
const EXCLUDED = new Set([
|
|
5
|
+
'host', 'connection', 'keep-alive', 'transfer-encoding', 'content-length',
|
|
6
|
+
'te', 'trailer', 'upgrade', 'expect', 'accept-encoding',
|
|
7
|
+
'cookie', 'set-cookie', 'forwarded', 'via', 'referer', 'origin', 'user-agent',
|
|
8
|
+
'x-real-ip', 'x-original-url', 'x-rewrite-url', 'x-http-method-override'
|
|
9
|
+
])
|
|
10
|
+
const PREFIXES = ['proxy-', 'dotenvx-', 'x-forwarded-', 'sec-']
|
|
11
|
+
|
|
12
|
+
module.exports = function eligibleHeaders (name) {
|
|
13
|
+
const lower = name.toLowerCase()
|
|
14
|
+
return /^[!#$%&'*+.^_`|~0-9a-z-]+$/.test(lower) &&
|
|
15
|
+
!EXCLUDED.has(lower) && !PREFIXES.some(prefix => lower.startsWith(prefix))
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { randomBytes } = require('node:crypto')
|
|
2
|
+
const { scan, upsert, encrypted } = require('@dotenvx/primitives')
|
|
3
|
+
|
|
4
|
+
// Remove proxy credentials before any local or Armor decryption is attempted.
|
|
5
|
+
module.exports = function prepareProxy (src, credentials, processEnv = {}, proxyRules = new Map()) {
|
|
6
|
+
const { parsed } = scan(src)
|
|
7
|
+
const publicKeys = Object.entries(parsed).filter(([name]) => name.startsWith('DOTENV_PUBLIC_KEY'))
|
|
8
|
+
for (const [name, values] of Object.entries(parsed)) {
|
|
9
|
+
if (!proxyRules.has(name)) continue
|
|
10
|
+
const replaced = values.map(value => {
|
|
11
|
+
if (!encrypted(value)) return value
|
|
12
|
+
if (publicKeys.length > 1) throw new Error('Credential proxy requires an unambiguous DOTENV_PUBLIC_KEY in its env file.')
|
|
13
|
+
const publicKey = publicKeys.length === 1 ? publicKeys[0][1].at(-1) : processEnv.DOTENV_PUBLIC_KEY
|
|
14
|
+
if (!publicKey || !/^(02|03)[0-9a-f]{64}$/i.test(publicKey)) {
|
|
15
|
+
throw new Error('Credential proxy requires DOTENV_PUBLIC_KEY alongside the encrypted credential.')
|
|
16
|
+
}
|
|
17
|
+
const placeholder = `dotenvx_proxy_${randomBytes(24).toString('hex')}`
|
|
18
|
+
credentials.push({ name, host: proxyRules.get(name), placeholder, ciphertext: value, publicKey })
|
|
19
|
+
return placeholder
|
|
20
|
+
})
|
|
21
|
+
src = upsert(src, name, replaced)
|
|
22
|
+
}
|
|
23
|
+
return src
|
|
24
|
+
}
|