UNPKG

8.64 kBJavaScriptView Raw
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 const vaultPath = _vaultPath(options)
52
53 // Parse .env.vault
54 const result = DotenvModule.configDotenv({ path: vaultPath })
55 if (!result.parsed) {
56 throw new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`)
57 }
58
59 // handle scenario for comma separated keys - for use with key rotation
60 // example: DOTENV_KEY="dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenv.org/vault/.env.vault?environment=prod"
61 const keys = _dotenvKey(options).split(',')
62 const length = keys.length
63
64 let decrypted
65 for (let i = 0; i < length; i++) {
66 try {
67 // Get full key
68 const key = keys[i].trim()
69
70 // Get instructions for decrypt
71 const attrs = _instructions(result, key)
72
73 // Decrypt
74 decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key)
75
76 break
77 } catch (error) {
78 // last key
79 if (i + 1 >= length) {
80 throw error
81 }
82 // try next key
83 }
84 }
85
86 // Parse decrypted .env string
87 return DotenvModule.parse(decrypted)
88}
89
90function _log (message) {
91 console.log(`[dotenv@${version}][INFO] ${message}`)
92}
93
94function _warn (message) {
95 console.log(`[dotenv@${version}][WARN] ${message}`)
96}
97
98function _debug (message) {
99 console.log(`[dotenv@${version}][DEBUG] ${message}`)
100}
101
102function _dotenvKey (options) {
103 // prioritize developer directly setting options.DOTENV_KEY
104 if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
105 return options.DOTENV_KEY
106 }
107
108 // secondary infra already contains a DOTENV_KEY environment variable
109 if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
110 return process.env.DOTENV_KEY
111 }
112
113 // fallback to empty string
114 return ''
115}
116
117function _instructions (result, dotenvKey) {
118 // Parse DOTENV_KEY. Format is a URI
119 let uri
120 try {
121 uri = new URL(dotenvKey)
122 } catch (error) {
123 if (error.code === 'ERR_INVALID_URL') {
124 throw new Error('INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=development')
125 }
126
127 throw error
128 }
129
130 // Get decrypt key
131 const key = uri.password
132 if (!key) {
133 throw new Error('INVALID_DOTENV_KEY: Missing key part')
134 }
135
136 // Get environment
137 const environment = uri.searchParams.get('environment')
138 if (!environment) {
139 throw new Error('INVALID_DOTENV_KEY: Missing environment part')
140 }
141
142 // Get ciphertext payload
143 const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
144 const ciphertext = result.parsed[environmentKey] // DOTENV_VAULT_PRODUCTION
145 if (!ciphertext) {
146 throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`)
147 }
148
149 return { ciphertext, key }
150}
151
152function _vaultPath (options) {
153 let dotenvPath = path.resolve(process.cwd(), '.env')
154
155 if (options && options.path && options.path.length > 0) {
156 dotenvPath = options.path
157 }
158
159 // Locate .env.vault
160 return dotenvPath.endsWith('.vault') ? dotenvPath : `${dotenvPath}.vault`
161}
162
163function _resolveHome (envPath) {
164 return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
165}
166
167function _configVault (options) {
168 _log('Loading env from encrypted .env.vault')
169
170 const parsed = DotenvModule._parseVault(options)
171
172 let processEnv = process.env
173 if (options && options.processEnv != null) {
174 processEnv = options.processEnv
175 }
176
177 DotenvModule.populate(processEnv, parsed, options)
178
179 return { parsed }
180}
181
182function configDotenv (options) {
183 let dotenvPath = path.resolve(process.cwd(), '.env')
184 let encoding = 'utf8'
185 const debug = Boolean(options && options.debug)
186
187 if (options) {
188 if (options.path != null) {
189 dotenvPath = _resolveHome(options.path)
190 }
191 if (options.encoding != null) {
192 encoding = options.encoding
193 } else {
194 if (debug) {
195 _debug('No encoding is specified. UTF-8 is used by default')
196 }
197 }
198 }
199
200 try {
201 // Specifying an encoding returns a string instead of a buffer
202 const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }))
203
204 let processEnv = process.env
205 if (options && options.processEnv != null) {
206 processEnv = options.processEnv
207 }
208
209 DotenvModule.populate(processEnv, parsed, options)
210
211 return { parsed }
212 } catch (e) {
213 if (debug) {
214 _debug(`Failed to load ${dotenvPath} ${e.message}`)
215 }
216
217 return { error: e }
218 }
219}
220
221// Populates process.env from .env file
222function config (options) {
223 const vaultPath = _vaultPath(options)
224
225 // fallback to original dotenv if DOTENV_KEY is not set
226 if (_dotenvKey(options).length === 0) {
227 return DotenvModule.configDotenv(options)
228 }
229
230 // dotenvKey exists but .env.vault file does not exist
231 if (!fs.existsSync(vaultPath)) {
232 _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`)
233
234 return DotenvModule.configDotenv(options)
235 }
236
237 return DotenvModule._configVault(options)
238}
239
240function decrypt (encrypted, keyStr) {
241 const key = Buffer.from(keyStr.slice(-64), 'hex')
242 let ciphertext = Buffer.from(encrypted, 'base64')
243
244 const nonce = ciphertext.subarray(0, 12)
245 const authTag = ciphertext.subarray(-16)
246 ciphertext = ciphertext.subarray(12, -16)
247
248 try {
249 const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce)
250 aesgcm.setAuthTag(authTag)
251 return `${aesgcm.update(ciphertext)}${aesgcm.final()}`
252 } catch (error) {
253 const isRange = error instanceof RangeError
254 const invalidKeyLength = error.message === 'Invalid key length'
255 const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data'
256
257 if (isRange || invalidKeyLength) {
258 const msg = 'INVALID_DOTENV_KEY: It must be 64 characters long (or more)'
259 throw new Error(msg)
260 } else if (decryptionFailed) {
261 const msg = 'DECRYPTION_FAILED: Please check your DOTENV_KEY'
262 throw new Error(msg)
263 } else {
264 console.error('Error: ', error.code)
265 console.error('Error: ', error.message)
266 throw error
267 }
268 }
269}
270
271// Populate process.env with parsed values
272function populate (processEnv, parsed, options = {}) {
273 const debug = Boolean(options && options.debug)
274 const override = Boolean(options && options.override)
275
276 if (typeof parsed !== 'object') {
277 throw new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate')
278 }
279
280 // Set process.env
281 for (const key of Object.keys(parsed)) {
282 if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
283 if (override === true) {
284 processEnv[key] = parsed[key]
285 }
286
287 if (debug) {
288 if (override === true) {
289 _debug(`"${key}" is already defined and WAS overwritten`)
290 } else {
291 _debug(`"${key}" is already defined and was NOT overwritten`)
292 }
293 }
294 } else {
295 processEnv[key] = parsed[key]
296 }
297 }
298}
299
300const DotenvModule = {
301 configDotenv,
302 _configVault,
303 _parseVault,
304 config,
305 decrypt,
306 parse,
307 populate
308}
309
310module.exports.configDotenv = DotenvModule.configDotenv
311module.exports._configVault = DotenvModule._configVault
312module.exports._parseVault = DotenvModule._parseVault
313module.exports.config = DotenvModule.config
314module.exports.decrypt = DotenvModule.decrypt
315module.exports.parse = DotenvModule.parse
316module.exports.populate = DotenvModule.populate
317
318module.exports = DotenvModule