source: node_modules/dotenv/lib/main.js@ 2d1ec46

main
Last change on this file since 2d1ec46 was 81bc7da, checked in by Klimentina Efremova <klimentina08642@…>, 3 months ago

Initial commit

  • Property mode set to 100644
File size: 10.5 KB
Line 
1const fs = require('fs')
2const path = require('path')
3const os = require('os')
4const crypto = require('crypto')
5const packageJson = require('../package.json')
6
7const version = packageJson.version
8
9const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
10
11// Parse src into an Object
12function parse (src) {
13 const obj = {}
14
15 // Convert buffer to string
16 let lines = src.toString()
17
18 // Convert line breaks to same format
19 lines = lines.replace(/\r\n?/mg, '\n')
20
21 let match
22 while ((match = LINE.exec(lines)) != null) {
23 const key = match[1]
24
25 // Default undefined or null to empty string
26 let value = (match[2] || '')
27
28 // Remove whitespace
29 value = value.trim()
30
31 // Check if double quoted
32 const maybeQuote = value[0]
33
34 // Remove surrounding quotes
35 value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
36
37 // Expand newlines if double quoted
38 if (maybeQuote === '"') {
39 value = value.replace(/\\n/g, '\n')
40 value = value.replace(/\\r/g, '\r')
41 }
42
43 // Add to object
44 obj[key] = value
45 }
46
47 return obj
48}
49
50function _parseVault (options) {
51 options = options || {}
52
53 const vaultPath = _vaultPath(options)
54 options.path = vaultPath // parse .env.vault
55 const result = DotenvModule.configDotenv(options)
56 if (!result.parsed) {
57 const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`)
58 err.code = 'MISSING_DATA'
59 throw err
60 }
61
62 // handle scenario for comma separated keys - for use with key rotation
63 // example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
64 const keys = _dotenvKey(options).split(',')
65 const length = keys.length
66
67 let decrypted
68 for (let i = 0; i < length; i++) {
69 try {
70 // Get full key
71 const key = keys[i].trim()
72
73 // Get instructions for decrypt
74 const attrs = _instructions(result, key)
75
76 // Decrypt
77 decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key)
78
79 break
80 } catch (error) {
81 // last key
82 if (i + 1 >= length) {
83 throw error
84 }
85 // try next key
86 }
87 }
88
89 // Parse decrypted .env string
90 return DotenvModule.parse(decrypted)
91}
92
93function _warn (message) {
94 console.log(`[dotenv@${version}][WARN] ${message}`)
95}
96
97function _debug (message) {
98 console.log(`[dotenv@${version}][DEBUG] ${message}`)
99}
100
101function _log (message) {
102 console.log(`[dotenv@${version}] ${message}`)
103}
104
105function _dotenvKey (options) {
106 // prioritize developer directly setting options.DOTENV_KEY
107 if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
108 return options.DOTENV_KEY
109 }
110
111 // secondary infra already contains a DOTENV_KEY environment variable
112 if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
113 return process.env.DOTENV_KEY
114 }
115
116 // fallback to empty string
117 return ''
118}
119
120function _instructions (result, dotenvKey) {
121 // Parse DOTENV_KEY. Format is a URI
122 let uri
123 try {
124 uri = new URL(dotenvKey)
125 } catch (error) {
126 if (error.code === 'ERR_INVALID_URL') {
127 const err = new Error('INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development')
128 err.code = 'INVALID_DOTENV_KEY'
129 throw err
130 }
131
132 throw error
133 }
134
135 // Get decrypt key
136 const key = uri.password
137 if (!key) {
138 const err = new Error('INVALID_DOTENV_KEY: Missing key part')
139 err.code = 'INVALID_DOTENV_KEY'
140 throw err
141 }
142
143 // Get environment
144 const environment = uri.searchParams.get('environment')
145 if (!environment) {
146 const err = new Error('INVALID_DOTENV_KEY: Missing environment part')
147 err.code = 'INVALID_DOTENV_KEY'
148 throw err
149 }
150
151 // Get ciphertext payload
152 const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
153 const ciphertext = result.parsed[environmentKey] // DOTENV_VAULT_PRODUCTION
154 if (!ciphertext) {
155 const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`)
156 err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT'
157 throw err
158 }
159
160 return { ciphertext, key }
161}
162
163function _vaultPath (options) {
164 let possibleVaultPath = null
165
166 if (options && options.path && options.path.length > 0) {
167 if (Array.isArray(options.path)) {
168 for (const filepath of options.path) {
169 if (fs.existsSync(filepath)) {
170 possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault`
171 }
172 }
173 } else {
174 possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault`
175 }
176 } else {
177 possibleVaultPath = path.resolve(process.cwd(), '.env.vault')
178 }
179
180 if (fs.existsSync(possibleVaultPath)) {
181 return possibleVaultPath
182 }
183
184 return null
185}
186
187function _resolveHome (envPath) {
188 return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
189}
190
191function _configVault (options) {
192 const debug = Boolean(options && options.debug)
193 const quiet = options && 'quiet' in options ? options.quiet : true
194
195 if (debug || !quiet) {
196 _log('Loading env from encrypted .env.vault')
197 }
198
199 const parsed = DotenvModule._parseVault(options)
200
201 let processEnv = process.env
202 if (options && options.processEnv != null) {
203 processEnv = options.processEnv
204 }
205
206 DotenvModule.populate(processEnv, parsed, options)
207
208 return { parsed }
209}
210
211function configDotenv (options) {
212 const dotenvPath = path.resolve(process.cwd(), '.env')
213 let encoding = 'utf8'
214 const debug = Boolean(options && options.debug)
215 const quiet = options && 'quiet' in options ? options.quiet : true
216
217 if (options && options.encoding) {
218 encoding = options.encoding
219 } else {
220 if (debug) {
221 _debug('No encoding is specified. UTF-8 is used by default')
222 }
223 }
224
225 let optionPaths = [dotenvPath] // default, look for .env
226 if (options && options.path) {
227 if (!Array.isArray(options.path)) {
228 optionPaths = [_resolveHome(options.path)]
229 } else {
230 optionPaths = [] // reset default
231 for (const filepath of options.path) {
232 optionPaths.push(_resolveHome(filepath))
233 }
234 }
235 }
236
237 // Build the parsed data in a temporary object (because we need to return it). Once we have the final
238 // parsed data, we will combine it with process.env (or options.processEnv if provided).
239 let lastError
240 const parsedAll = {}
241 for (const path of optionPaths) {
242 try {
243 // Specifying an encoding returns a string instead of a buffer
244 const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }))
245
246 DotenvModule.populate(parsedAll, parsed, options)
247 } catch (e) {
248 if (debug) {
249 _debug(`Failed to load ${path} ${e.message}`)
250 }
251 lastError = e
252 }
253 }
254
255 let processEnv = process.env
256 if (options && options.processEnv != null) {
257 processEnv = options.processEnv
258 }
259
260 DotenvModule.populate(processEnv, parsedAll, options)
261
262 if (debug || !quiet) {
263 const keysCount = Object.keys(parsedAll).length
264 const shortPaths = []
265 for (const filePath of optionPaths) {
266 try {
267 const relative = path.relative(process.cwd(), filePath)
268 shortPaths.push(relative)
269 } catch (e) {
270 if (debug) {
271 _debug(`Failed to load ${filePath} ${e.message}`)
272 }
273 lastError = e
274 }
275 }
276
277 _log(`injecting env (${keysCount}) from ${shortPaths.join(',')}`)
278 }
279
280 if (lastError) {
281 return { parsed: parsedAll, error: lastError }
282 } else {
283 return { parsed: parsedAll }
284 }
285}
286
287// Populates process.env from .env file
288function config (options) {
289 // fallback to original dotenv if DOTENV_KEY is not set
290 if (_dotenvKey(options).length === 0) {
291 return DotenvModule.configDotenv(options)
292 }
293
294 const vaultPath = _vaultPath(options)
295
296 // dotenvKey exists but .env.vault file does not exist
297 if (!vaultPath) {
298 _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`)
299
300 return DotenvModule.configDotenv(options)
301 }
302
303 return DotenvModule._configVault(options)
304}
305
306function decrypt (encrypted, keyStr) {
307 const key = Buffer.from(keyStr.slice(-64), 'hex')
308 let ciphertext = Buffer.from(encrypted, 'base64')
309
310 const nonce = ciphertext.subarray(0, 12)
311 const authTag = ciphertext.subarray(-16)
312 ciphertext = ciphertext.subarray(12, -16)
313
314 try {
315 const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce)
316 aesgcm.setAuthTag(authTag)
317 return `${aesgcm.update(ciphertext)}${aesgcm.final()}`
318 } catch (error) {
319 const isRange = error instanceof RangeError
320 const invalidKeyLength = error.message === 'Invalid key length'
321 const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data'
322
323 if (isRange || invalidKeyLength) {
324 const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)')
325 err.code = 'INVALID_DOTENV_KEY'
326 throw err
327 } else if (decryptionFailed) {
328 const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY')
329 err.code = 'DECRYPTION_FAILED'
330 throw err
331 } else {
332 throw error
333 }
334 }
335}
336
337// Populate process.env with parsed values
338function populate (processEnv, parsed, options = {}) {
339 const debug = Boolean(options && options.debug)
340 const override = Boolean(options && options.override)
341
342 if (typeof parsed !== 'object') {
343 const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate')
344 err.code = 'OBJECT_REQUIRED'
345 throw err
346 }
347
348 // Set process.env
349 for (const key of Object.keys(parsed)) {
350 if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
351 if (override === true) {
352 processEnv[key] = parsed[key]
353 }
354
355 if (debug) {
356 if (override === true) {
357 _debug(`"${key}" is already defined and WAS overwritten`)
358 } else {
359 _debug(`"${key}" is already defined and was NOT overwritten`)
360 }
361 }
362 } else {
363 processEnv[key] = parsed[key]
364 }
365 }
366}
367
368const DotenvModule = {
369 configDotenv,
370 _configVault,
371 _parseVault,
372 config,
373 decrypt,
374 parse,
375 populate
376}
377
378module.exports.configDotenv = DotenvModule.configDotenv
379module.exports._configVault = DotenvModule._configVault
380module.exports._parseVault = DotenvModule._parseVault
381module.exports.config = DotenvModule.config
382module.exports.decrypt = DotenvModule.decrypt
383module.exports.parse = DotenvModule.parse
384module.exports.populate = DotenvModule.populate
385
386module.exports = DotenvModule
Note: See TracBrowser for help on using the repository browser.