@dotenvx/dotenvx 2.26.1 → 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 +7 -1
- 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/dotenvx.js +2 -2
- package/src/lib/grammars/envfile.peggy +98 -0
- 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/parseWithDecryptor.js +9 -1
- package/src/lib/helpers/readEnvfile.js +128 -0
- package/src/lib/helpers/selectKeyStorage.js +1 -1
- 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/validate.js +58 -0
- package/src/lib/transforms/set.js +2 -2
- package/src/lib/helpers/validateEnvExample.js +0 -24
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { generateKeyPair, randomBytes } = require('node:crypto')
|
|
2
|
+
const { promisify } = require('node:util')
|
|
3
|
+
const forge = require('node-forge')
|
|
4
|
+
|
|
5
|
+
// Only public certificates are persisted. Both signing keys stay in this process.
|
|
6
|
+
module.exports = async function proxyCertificates (hosts = ['localhost']) {
|
|
7
|
+
const generate = () => promisify(generateKeyPair)('rsa', {
|
|
8
|
+
modulusLength: 2048,
|
|
9
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
10
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
11
|
+
})
|
|
12
|
+
const [caKeys, leafKeys] = await Promise.all([generate(), generate()])
|
|
13
|
+
const issuer = [{ name: 'commonName', value: 'dotenvx temporary proxy' }]
|
|
14
|
+
const certificate = (keys, subject) => {
|
|
15
|
+
const cert = forge.pki.createCertificate()
|
|
16
|
+
cert.publicKey = forge.pki.publicKeyFromPem(keys.publicKey)
|
|
17
|
+
cert.serialNumber = '01' + randomBytes(16).toString('hex')
|
|
18
|
+
cert.validity.notBefore = new Date(Date.now() - 60000)
|
|
19
|
+
cert.validity.notAfter = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
|
20
|
+
cert.setSubject(subject)
|
|
21
|
+
cert.setIssuer(issuer)
|
|
22
|
+
return cert
|
|
23
|
+
}
|
|
24
|
+
const ca = certificate(caKeys, issuer)
|
|
25
|
+
ca.setExtensions([
|
|
26
|
+
{ name: 'basicConstraints', cA: true, critical: true },
|
|
27
|
+
{ name: 'subjectKeyIdentifier' },
|
|
28
|
+
{ name: 'authorityKeyIdentifier', keyIdentifier: true },
|
|
29
|
+
{ name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true }
|
|
30
|
+
])
|
|
31
|
+
const signingKey = forge.pki.privateKeyFromPem(caKeys.privateKey)
|
|
32
|
+
ca.sign(signingKey, forge.md.sha256.create())
|
|
33
|
+
const leaf = certificate(leafKeys, [{ name: 'commonName', value: hosts[0] || 'localhost' }])
|
|
34
|
+
leaf.setExtensions([
|
|
35
|
+
{ name: 'basicConstraints', cA: false, critical: true },
|
|
36
|
+
{ name: 'subjectKeyIdentifier' },
|
|
37
|
+
{ name: 'authorityKeyIdentifier', keyIdentifier: ca.generateSubjectKeyIdentifier().getBytes() },
|
|
38
|
+
{ name: 'keyUsage', digitalSignature: true, keyEncipherment: true, critical: true },
|
|
39
|
+
{ name: 'extKeyUsage', serverAuth: true },
|
|
40
|
+
{ name: 'subjectAltName', altNames: hosts.map(value => ({ type: 2, value })) }
|
|
41
|
+
])
|
|
42
|
+
leaf.sign(signingKey, forge.md.sha256.create())
|
|
43
|
+
return { ca: forge.pki.certificateToPem(ca), cert: forge.pki.certificateToPem(leaf), key: leafKeys.privateKey }
|
|
44
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const eligibleHeaders = require('./eligibleHeaders')
|
|
2
|
+
|
|
3
|
+
// Parent-only forwarding. Never load credential configuration into the application.
|
|
4
|
+
function install (config, nativeFetch = globalThis.fetch) {
|
|
5
|
+
if (typeof nativeFetch !== 'function') throw new Error('Credential proxy requires Node.js with built-in fetch.')
|
|
6
|
+
return async function proxyFetch (input, init) {
|
|
7
|
+
const url = new URL(input instanceof Request ? input.url : input)
|
|
8
|
+
if (!config.credentials.some(item => item.host === url.hostname)) return nativeFetch(input, init)
|
|
9
|
+
if (url.protocol !== 'https:' || (url.port && url.port !== '443') || url.username || url.password) {
|
|
10
|
+
throw new Error('Credential proxy requests must use HTTPS on port 443.')
|
|
11
|
+
}
|
|
12
|
+
const request = new Request(input, init)
|
|
13
|
+
const upstreamHeaders = Object.fromEntries([...request.headers].filter(([name]) => eligibleHeaders(name)))
|
|
14
|
+
const values = Object.entries(upstreamHeaders).map(([name, value]) => {
|
|
15
|
+
// Preserve existing Basic support: its placeholder is encoded on the wire.
|
|
16
|
+
if (name === 'authorization' && value.startsWith('Basic ')) {
|
|
17
|
+
const encoded = value.slice(6)
|
|
18
|
+
const decoded = Buffer.from(encoded, 'base64')
|
|
19
|
+
if (decoded.toString('base64') !== encoded) throw new Error('Invalid Basic authorization.')
|
|
20
|
+
return decoded.toString('utf8')
|
|
21
|
+
}
|
|
22
|
+
return value
|
|
23
|
+
})
|
|
24
|
+
const active = config.credentials.filter(credential => {
|
|
25
|
+
const occurrences = values.reduce((total, value) => total + value.split(credential.placeholder).length - 1, 0)
|
|
26
|
+
if (occurrences && credential.host !== url.hostname) throw new Error('Proxy credential is not allowed at this destination.')
|
|
27
|
+
if (occurrences > 1) throw new Error('Proxy credential must occur in only one eligible header, once.')
|
|
28
|
+
return occurrences === 1
|
|
29
|
+
})
|
|
30
|
+
if (!active.length) throw new Error('Credential proxy requires a proxied environment variable in an eligible header.')
|
|
31
|
+
if (active.length > 8 || active.some(item => item.publicKey !== active[0].publicKey)) {
|
|
32
|
+
throw new Error('Proxy request requires at most eight credentials from the same keypair.')
|
|
33
|
+
}
|
|
34
|
+
const body = ['GET', 'HEAD'].includes(request.method) ? null : Buffer.from(await request.arrayBuffer())
|
|
35
|
+
if (body && body.length > 256 * 1024) throw new Error('Proxy request body exceeds 256 KiB.')
|
|
36
|
+
const credentials = active.map(({ publicKey, placeholder, ciphertext }) => ({ publicKey, placeholder, ciphertext }))
|
|
37
|
+
const metadata = JSON.stringify({ headers: upstreamHeaders, credentials })
|
|
38
|
+
if (Buffer.byteLength(metadata) > 64 * 1024) throw new Error('Proxy request metadata exceeds 64 KiB.')
|
|
39
|
+
const payload = JSON.stringify({
|
|
40
|
+
version: 1,
|
|
41
|
+
request: {
|
|
42
|
+
url: url.href,
|
|
43
|
+
method: request.method,
|
|
44
|
+
headers: upstreamHeaders,
|
|
45
|
+
body: body === null ? null : { encoding: 'base64', data: body.toString('base64') }
|
|
46
|
+
},
|
|
47
|
+
credentials
|
|
48
|
+
})
|
|
49
|
+
if (Buffer.byteLength(payload) > 512 * 1024) throw new Error('Proxy request envelope exceeds 512 KiB.')
|
|
50
|
+
const headers = new Headers({
|
|
51
|
+
Authorization: `Bearer ${config.token}`,
|
|
52
|
+
'Content-Type': 'application/json'
|
|
53
|
+
})
|
|
54
|
+
if (config.devicePublicKey) headers.set('dotenvx-device-public-key', config.devicePublicKey)
|
|
55
|
+
return nativeFetch(`${config.hostname}/api/proxy`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers,
|
|
58
|
+
body: payload,
|
|
59
|
+
signal: request.signal,
|
|
60
|
+
redirect: 'error'
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = install
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Other runtimes use standard proxy/CA variables. Node also needs a bridge for
|
|
2
|
+
// older runtimes and core HTTP clients that do not read proxy variables.
|
|
3
|
+
if (process.env.DOTENVX_PROXY_URL) {
|
|
4
|
+
const http = require('node:http')
|
|
5
|
+
if (typeof http.setGlobalProxyFromEnv === 'function') {
|
|
6
|
+
http.setGlobalProxyFromEnv()
|
|
7
|
+
} else {
|
|
8
|
+
const { ProxyAgent, Agent, Dispatcher, setGlobalDispatcher } = require('undici')
|
|
9
|
+
const proxy = new ProxyAgent(process.env.DOTENVX_PROXY_URL)
|
|
10
|
+
const direct = new Agent()
|
|
11
|
+
class ProxyDispatcher extends Dispatcher {
|
|
12
|
+
dispatch (options, handler) {
|
|
13
|
+
const host = new URL(options.origin).hostname
|
|
14
|
+
return (['localhost', '127.0.0.1', '[::1]'].includes(host) ? direct : proxy).dispatch(options, handler)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
setGlobalDispatcher(new ProxyDispatcher())
|
|
18
|
+
}
|
|
19
|
+
const { createGlobalProxyAgent } = require('global-agent')
|
|
20
|
+
createGlobalProxyAgent({ environmentVariableNamespace: '', forceGlobalAgent: true })
|
|
21
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const http = require('node:http')
|
|
2
|
+
const https = require('node:https')
|
|
3
|
+
const net = require('node:net')
|
|
4
|
+
const tls = require('node:tls')
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const os = require('node:os')
|
|
7
|
+
const path = require('node:path')
|
|
8
|
+
const certificates = require('./proxyCertificates')
|
|
9
|
+
const forward = require('./proxyForward')
|
|
10
|
+
const eligibleHeaders = require('./eligibleHeaders')
|
|
11
|
+
|
|
12
|
+
const REQUEST_LIMIT = 256 * 1024
|
|
13
|
+
const RESPONSE_LIMIT = 2 * 1024 * 1024
|
|
14
|
+
const RESPONSE_HEADERS = ['content-type', 'request-id', 'retry-after', 'stripe-version']
|
|
15
|
+
|
|
16
|
+
module.exports = async function startProxy (config, nativeFetch) {
|
|
17
|
+
const fetch = forward(config, nativeFetch)
|
|
18
|
+
const hosts = new Set(config.credentials.map(item => item.host))
|
|
19
|
+
const identity = await certificates(hosts.size ? [...hosts] : ['localhost'])
|
|
20
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'dxp-'))
|
|
21
|
+
fs.chmodSync(directory, 0o700)
|
|
22
|
+
const caPath = path.join(directory, 'ca.pem')
|
|
23
|
+
const sockets = new Set()
|
|
24
|
+
const track = socket => {
|
|
25
|
+
sockets.add(socket)
|
|
26
|
+
socket.on('close', () => sockets.delete(socket))
|
|
27
|
+
socket.on('error', () => {})
|
|
28
|
+
socket.setTimeout(30000, () => socket.destroy())
|
|
29
|
+
return socket
|
|
30
|
+
}
|
|
31
|
+
const reject = socket => socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n')
|
|
32
|
+
const servers = new Map()
|
|
33
|
+
for (const host of hosts) {
|
|
34
|
+
const secure = https.createServer({ key: identity.key, cert: identity.cert, ALPNProtocols: ['http/1.1'] }, async (req, res) => {
|
|
35
|
+
const controller = new AbortController()
|
|
36
|
+
res.on('close', () => { if (!res.writableEnded) controller.abort() })
|
|
37
|
+
try {
|
|
38
|
+
const url = new URL(req.url, `https://${host}`)
|
|
39
|
+
if (![host, `${host}:443`].includes(req.headers.host?.toLowerCase()) ||
|
|
40
|
+
url.origin !== `https://${host}` || url.username || url.password || url.hash ||
|
|
41
|
+
!req.url.startsWith('/') || req.url.startsWith('//') ||
|
|
42
|
+
!['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
|
|
43
|
+
res.writeHead(403).end('Proxy request is not allowed.')
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
const chunks = []
|
|
47
|
+
let size = 0
|
|
48
|
+
for await (const chunk of req) {
|
|
49
|
+
size += chunk.length
|
|
50
|
+
if (size > REQUEST_LIMIT) {
|
|
51
|
+
res.writeHead(413).end('Proxy request exceeds 256 KiB.')
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
chunks.push(chunk)
|
|
55
|
+
}
|
|
56
|
+
const headers = new Headers()
|
|
57
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
58
|
+
if (eligibleHeaders(name)) headers.set(name, Array.isArray(value) ? value.join(', ') : value)
|
|
59
|
+
}
|
|
60
|
+
const response = await fetch(url.href, {
|
|
61
|
+
method: req.method,
|
|
62
|
+
headers,
|
|
63
|
+
body: ['GET', 'HEAD'].includes(req.method) ? undefined : Buffer.concat(chunks),
|
|
64
|
+
signal: controller.signal
|
|
65
|
+
})
|
|
66
|
+
const output = []
|
|
67
|
+
let bytes = 0
|
|
68
|
+
if (response.body) {
|
|
69
|
+
for await (const chunk of response.body) {
|
|
70
|
+
bytes += chunk.length
|
|
71
|
+
if (bytes > RESPONSE_LIMIT) throw new Error('response too large')
|
|
72
|
+
output.push(Buffer.from(chunk))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const name of RESPONSE_HEADERS) {
|
|
76
|
+
if (response.headers.has(name)) res.setHeader(name, response.headers.get(name))
|
|
77
|
+
}
|
|
78
|
+
res.writeHead(response.status).end(Buffer.concat(output))
|
|
79
|
+
} catch {
|
|
80
|
+
if (!res.headersSent) res.writeHead(502)
|
|
81
|
+
res.end('Proxy request failed.')
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
secure.on('secureConnection', track)
|
|
85
|
+
secure.on('tlsClientError', (_error, socket) => socket.destroy())
|
|
86
|
+
secure.on('upgrade', (_req, socket) => reject(socket))
|
|
87
|
+
secure.requestTimeout = 30000
|
|
88
|
+
secure.headersTimeout = 10000
|
|
89
|
+
servers.set(host, secure)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const server = http.createServer((req, res) => {
|
|
93
|
+
// Ordinary HTTP is forwarded without injecting any proxy credential.
|
|
94
|
+
let url
|
|
95
|
+
try { url = new URL(req.url) } catch { res.writeHead(400).end(); return }
|
|
96
|
+
if (url.protocol !== 'http:' || url.username || url.password || hosts.has(url.hostname) || self(url)) {
|
|
97
|
+
res.writeHead(403).end('Proxy request is not allowed.')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
const headers = { ...req.headers, host: url.host }
|
|
101
|
+
for (const name of ['proxy-authorization', 'proxy-connection', 'connection']) delete headers[name]
|
|
102
|
+
const upstream = http.request(url, { method: req.method, headers, agent: false }, response => {
|
|
103
|
+
const responseHeaders = { ...response.headers }
|
|
104
|
+
delete responseHeaders['proxy-authenticate']
|
|
105
|
+
res.writeHead(response.statusCode, responseHeaders)
|
|
106
|
+
response.pipe(res)
|
|
107
|
+
response.on('error', () => res.destroy())
|
|
108
|
+
})
|
|
109
|
+
upstream.on('socket', track)
|
|
110
|
+
upstream.on('error', () => { if (!res.headersSent) res.writeHead(502); res.end('Proxy request failed.') })
|
|
111
|
+
res.on('close', () => upstream.destroy())
|
|
112
|
+
req.pipe(upstream)
|
|
113
|
+
})
|
|
114
|
+
function self (url) {
|
|
115
|
+
return ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) && Number(url.port) === server.address()?.port
|
|
116
|
+
}
|
|
117
|
+
server.on('connection', track)
|
|
118
|
+
server.on('upgrade', (_req, socket) => reject(socket))
|
|
119
|
+
server.on('connect', (req, socket, head) => {
|
|
120
|
+
let target
|
|
121
|
+
try {
|
|
122
|
+
if (!/^(\[[\da-f:]+\]|[a-z\d.-]+):\d+$/i.test(req.url)) throw new Error('invalid authority')
|
|
123
|
+
target = new URL(`https://${req.url}`)
|
|
124
|
+
if (self(target)) throw new Error('proxy loop')
|
|
125
|
+
} catch { reject(socket); return }
|
|
126
|
+
if (hosts.has(target.hostname)) {
|
|
127
|
+
if (target.port && target.port !== '443') { reject(socket); return }
|
|
128
|
+
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
|
|
129
|
+
if (head.length) socket.unshift(head)
|
|
130
|
+
servers.get(target.hostname).emit('connection', socket)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
// Unmanaged HTTPS remains end-to-end encrypted through a standard CONNECT tunnel.
|
|
134
|
+
const upstream = track(net.connect(Number(target.port) || 443, target.hostname.replace(/^\[|\]$/g, '')))
|
|
135
|
+
upstream.on('connect', () => {
|
|
136
|
+
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
|
|
137
|
+
if (head.length) upstream.write(head)
|
|
138
|
+
socket.pipe(upstream).pipe(socket)
|
|
139
|
+
})
|
|
140
|
+
upstream.on('error', () => socket.destroy())
|
|
141
|
+
socket.on('close', () => upstream.destroy())
|
|
142
|
+
upstream.on('close', () => socket.destroy())
|
|
143
|
+
})
|
|
144
|
+
server.requestTimeout = 30000
|
|
145
|
+
server.headersTimeout = 10000
|
|
146
|
+
let closed
|
|
147
|
+
const close = () => {
|
|
148
|
+
if (!closed) {
|
|
149
|
+
closed = (async () => {
|
|
150
|
+
for (const socket of sockets) socket.destroy()
|
|
151
|
+
await new Promise(resolve => server.close(resolve))
|
|
152
|
+
fs.rmSync(directory, { recursive: true, force: true })
|
|
153
|
+
})()
|
|
154
|
+
}
|
|
155
|
+
return closed
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const roots = typeof tls.getCACertificates === 'function' ? tls.getCACertificates('default') : tls.rootCertificates
|
|
159
|
+
const trust = new Set(roots)
|
|
160
|
+
const env = config.env || {}
|
|
161
|
+
for (const name of ['NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE', 'GIT_SSL_CAINFO', 'CARGO_HTTP_CAINFO', 'DENO_CERT']) {
|
|
162
|
+
if (env[name]) trust.add(fs.readFileSync(env[name], 'utf8'))
|
|
163
|
+
}
|
|
164
|
+
trust.add(identity.ca)
|
|
165
|
+
fs.writeFileSync(caPath, [...trust].join('\n'), { mode: 0o600 })
|
|
166
|
+
await new Promise((resolve, reject) => {
|
|
167
|
+
server.once('error', reject)
|
|
168
|
+
server.listen(0, '127.0.0.1', () => { server.removeListener('error', reject); resolve() })
|
|
169
|
+
})
|
|
170
|
+
} catch (error) {
|
|
171
|
+
await close()
|
|
172
|
+
throw error
|
|
173
|
+
}
|
|
174
|
+
return { proxyUrl: `http://127.0.0.1:${server.address().port}`, caPath, close }
|
|
175
|
+
}
|
|
@@ -54,10 +54,12 @@ function inject (processEnv, parsed) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function buildParseOptions ({ processEnv, overload, envKeysFilepath, provider, decryptor }) {
|
|
57
|
+
function buildParseOptions ({ processEnv, overload, envKeysFilepath, provider, decryptor, proxyCredentials, proxyRules }) {
|
|
58
58
|
const options = {
|
|
59
59
|
processEnv,
|
|
60
60
|
overload,
|
|
61
|
+
proxyCredentials,
|
|
62
|
+
proxyRules,
|
|
61
63
|
fk: envKeysFilepath
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -74,7 +76,7 @@ function buildParseOptions ({ processEnv, overload, envKeysFilepath, provider, d
|
|
|
74
76
|
return options
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, no1Password, noBitwarden, onStatus }) {
|
|
79
|
+
async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, no1Password, noBitwarden, onStatus, proxyCredentials, proxyRules }) {
|
|
78
80
|
const row = {}
|
|
79
81
|
row.type = TYPE_ENV
|
|
80
82
|
row.string = env.value
|
|
@@ -90,7 +92,9 @@ async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider
|
|
|
90
92
|
overload,
|
|
91
93
|
envKeysFilepath,
|
|
92
94
|
provider,
|
|
93
|
-
decryptor
|
|
95
|
+
decryptor,
|
|
96
|
+
proxyCredentials,
|
|
97
|
+
proxyRules
|
|
94
98
|
})
|
|
95
99
|
|
|
96
100
|
const {
|
|
@@ -179,7 +183,7 @@ function injectEnvSync ({ env, overload, processEnv, envKeysFilepath, provider,
|
|
|
179
183
|
return row
|
|
180
184
|
}
|
|
181
185
|
|
|
182
|
-
async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, readableFilepaths, no1Password, noBitwarden, onStatus }) {
|
|
186
|
+
async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, readableFilepaths, no1Password, noBitwarden, onStatus, proxyCredentials, proxyRules }) {
|
|
183
187
|
const row = {}
|
|
184
188
|
row.type = TYPE_ENV_FILE
|
|
185
189
|
row.filepath = env.value
|
|
@@ -197,7 +201,9 @@ async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, prov
|
|
|
197
201
|
overload,
|
|
198
202
|
envKeysFilepath: fk,
|
|
199
203
|
provider,
|
|
200
|
-
decryptor
|
|
204
|
+
decryptor,
|
|
205
|
+
proxyCredentials,
|
|
206
|
+
proxyRules
|
|
201
207
|
})
|
|
202
208
|
|
|
203
209
|
const {
|
|
@@ -319,6 +325,8 @@ async function envs (options = {}) {
|
|
|
319
325
|
readableFilepaths,
|
|
320
326
|
no1Password,
|
|
321
327
|
noBitwarden,
|
|
328
|
+
proxyCredentials: options.proxyCredentials,
|
|
329
|
+
proxyRules: options.proxyRules,
|
|
322
330
|
onStatus: options.onStatus
|
|
323
331
|
}))
|
|
324
332
|
} else if (env.type === TYPE_ENV) {
|
|
@@ -331,6 +339,8 @@ async function envs (options = {}) {
|
|
|
331
339
|
decryptor,
|
|
332
340
|
no1Password,
|
|
333
341
|
noBitwarden,
|
|
342
|
+
proxyCredentials: options.proxyCredentials,
|
|
343
|
+
proxyRules: options.proxyRules,
|
|
334
344
|
onStatus: options.onStatus
|
|
335
345
|
}))
|
|
336
346
|
}
|
package/src/lib/resolvers/get.js
CHANGED
|
@@ -50,7 +50,7 @@ function buildOptions (options, processEnv) {
|
|
|
50
50
|
processEnv,
|
|
51
51
|
envKeysFilepath: options.envKeysFilepath || options.envKeysFile || null,
|
|
52
52
|
noArmor: options.noArmor,
|
|
53
|
-
|
|
53
|
+
noNative: options.noNative,
|
|
54
54
|
no1Password: options.no1Password,
|
|
55
55
|
noBitwarden: options.noBitwarden,
|
|
56
56
|
onStatus: options.onStatus
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const envsResolver = require('../resolvers/envs')
|
|
2
|
+
const Session = require('../../db/session')
|
|
3
|
+
const buildCommandEnvs = require('../helpers/buildCommandEnvs')
|
|
4
|
+
const resolveEnvKeysFile = require('../helpers/resolveEnvKeysFile')
|
|
5
|
+
const readEnvfile = require('../helpers/readEnvfile')
|
|
6
|
+
const validateEnvfile = require('../helpers/validateEnvfile')
|
|
7
|
+
const normalizeDotenvConfigPath = require('../helpers/normalizeDotenvConfigPath')
|
|
8
|
+
const Errors = require('../helpers/errors')
|
|
9
|
+
const { determine } = require('../helpers/envResolution')
|
|
10
|
+
|
|
11
|
+
// Load once and validate the final values; callers own presentation and execution.
|
|
12
|
+
module.exports = async function validate ({ envs = [], options = {}, processEnv = { ...process.env }, requireEnvfile = true, command, onStatus } = {}) {
|
|
13
|
+
envs = buildCommandEnvs(normalizeDotenvConfigPath(envs, processEnv), options.convention)
|
|
14
|
+
envs = determine(envs, processEnv)
|
|
15
|
+
const schema = readEnvfile(undefined, envs.filter(env => env.type === 'envFile').map(env => env.value))
|
|
16
|
+
if (requireEnvfile && !schema.exists) throw new Errors().envfileRequired()
|
|
17
|
+
|
|
18
|
+
const session = new Session()
|
|
19
|
+
const proxyToken = options.token || processEnv.DOTENVX_TOKEN
|
|
20
|
+
const noArmor = options.armor === false || (!proxyToken && (await session.noArmor()))
|
|
21
|
+
if (schema.proxyRules.size > 0 && noArmor) throw new Error('Envfile proxy requires Armor. Enable Armor and authenticate before running.')
|
|
22
|
+
const proxyCredentials = noArmor ? undefined : []
|
|
23
|
+
const { processedEnvs, readableFilepaths } = await envsResolver({
|
|
24
|
+
envs,
|
|
25
|
+
proxyRules: schema.proxyRules,
|
|
26
|
+
proxyCredentials,
|
|
27
|
+
overload: options.overload,
|
|
28
|
+
processEnv,
|
|
29
|
+
envKeysFile: resolveEnvKeysFile(options.envKeysFile),
|
|
30
|
+
noArmor,
|
|
31
|
+
noNative: options.native === false || options.noNative === true,
|
|
32
|
+
no1Password: options['1password'] === false || options.no1Password === true,
|
|
33
|
+
noBitwarden: options.bitwarden === false || options.noBitwarden === true,
|
|
34
|
+
token: options.token,
|
|
35
|
+
command,
|
|
36
|
+
onStatus
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
let proxyError
|
|
40
|
+
for (const name of schema.proxyRules.keys()) {
|
|
41
|
+
if (processEnv[name] !== undefined && !(proxyCredentials || []).some(credential => credential.name === name && credential.placeholder === processEnv[name])) {
|
|
42
|
+
proxyError = new Error(`Envfile proxy requires an encrypted ${name} loaded from an env file. Remove plaintext or shell overrides, or use --overload.`)
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
processEnv,
|
|
49
|
+
processedEnvs,
|
|
50
|
+
readableFilepaths,
|
|
51
|
+
hasEnvfile: schema.exists,
|
|
52
|
+
proxyCredentials,
|
|
53
|
+
proxyToken,
|
|
54
|
+
session,
|
|
55
|
+
proxyError,
|
|
56
|
+
validationError: validateEnvfile(schema, processEnv, processedEnvs)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -28,7 +28,7 @@ async function setTransform (options = {}) {
|
|
|
28
28
|
const fk = options.fk || '.env.keys'
|
|
29
29
|
const noArmor = options.noArmor
|
|
30
30
|
let storage
|
|
31
|
-
const
|
|
31
|
+
const noNative = options.noNative
|
|
32
32
|
const noCreate = options.noCreate
|
|
33
33
|
const noEncrypt = !options.encrypt || isPlainKey(key)
|
|
34
34
|
|
|
@@ -147,7 +147,7 @@ async function setTransform (options = {}) {
|
|
|
147
147
|
all: true,
|
|
148
148
|
envKeysFile: fk,
|
|
149
149
|
noArmor,
|
|
150
|
-
|
|
150
|
+
noNative,
|
|
151
151
|
no1Password: options.no1Password,
|
|
152
152
|
noBitwarden: options.noBitwarden
|
|
153
153
|
})
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const fs = require('fs')
|
|
2
|
-
const { scan } = require('@dotenvx/primitives')
|
|
3
|
-
|
|
4
|
-
const Errors = require('./errors')
|
|
5
|
-
const validate = require('./validate')
|
|
6
|
-
|
|
7
|
-
function validateEnvExample (env = process.env, options = {}) {
|
|
8
|
-
const filepath = options.filepath || '.env.example'
|
|
9
|
-
|
|
10
|
-
if (!fs.existsSync(filepath)) {
|
|
11
|
-
return new Errors().missingEnvExample()
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const exampleSrc = fs.readFileSync(filepath, 'utf8')
|
|
15
|
-
const { parsed: example, comments } = scan(exampleSrc)
|
|
16
|
-
const validation = validate(example, env, { comments })
|
|
17
|
-
|
|
18
|
-
if (!validation.valid) {
|
|
19
|
-
const message = validation.errors.map(error => error.message).join('; ')
|
|
20
|
-
return new Errors({ message }).validationFailed()
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
module.exports = validateEnvExample
|