UNPKG

7.99 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().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 () {
103 if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
104 return process.env.DOTENV_KEY
105 }
106
107 return ''
108}
109
110function _instructions (result, dotenvKey) {
111 // Parse DOTENV_KEY. Format is a URI
112 let uri
113 try {
114 uri = new URL(dotenvKey)
115 } catch (error) {
116 if (error.code === 'ERR_INVALID_URL') {
117 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')
118 }
119
120 throw error
121 }
122
123 // Get decrypt key
124 const key = uri.password
125 if (!key) {
126 throw new Error('INVALID_DOTENV_KEY: Missing key part')
127 }
128
129 // Get environment
130 const environment = uri.searchParams.get('environment')
131 if (!environment) {
132 throw new Error('INVALID_DOTENV_KEY: Missing environment part')
133 }
134
135 // Get ciphertext payload
136 const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
137 const ciphertext = result.parsed[environmentKey] // DOTENV_VAULT_PRODUCTION
138 if (!ciphertext) {
139 throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`)
140 }
141
142 return { ciphertext, key }
143}
144
145function _vaultPath (options) {
146 let dotenvPath = path.resolve(process.cwd(), '.env')
147
148 if (options && options.path && options.path.length > 0) {
149 dotenvPath = options.path
150 }
151
152 // Locate .env.vault
153 return dotenvPath.endsWith('.vault') ? dotenvPath : `${dotenvPath}.vault`
154}
155
156function _resolveHome (envPath) {
157 return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
158}
159
160function _configVault (options) {
161 _log('Loading env from encrypted .env.vault')
162
163 const parsed = DotenvModule._parseVault(options)
164
165 DotenvModule.populate(process.env, parsed, options)
166
167 return { parsed }
168}
169
170function configDotenv (options) {
171 let dotenvPath = path.resolve(process.cwd(), '.env')
172 let encoding = 'utf8'
173 const debug = Boolean(options && options.debug)
174
175 if (options) {
176 if (options.path != null) {
177 dotenvPath = _resolveHome(options.path)
178 }
179 if (options.encoding != null) {
180 encoding = options.encoding
181 }
182 }
183
184 try {
185 // Specifying an encoding returns a string instead of a buffer
186 const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }))
187
188 DotenvModule.populate(process.env, parsed, options)
189
190 return { parsed }
191 } catch (e) {
192 if (debug) {
193 _debug(`Failed to load ${dotenvPath} ${e.message}`)
194 }
195
196 return { error: e }
197 }
198}
199
200// Populates process.env from .env file
201function config (options) {
202 const vaultPath = _vaultPath(options)
203
204 // fallback to original dotenv if DOTENV_KEY is not set
205 if (_dotenvKey().length === 0) {
206 return DotenvModule.configDotenv(options)
207 }
208
209 // dotenvKey exists but .env.vault file does not exist
210 if (!fs.existsSync(vaultPath)) {
211 _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`)
212
213 return DotenvModule.configDotenv(options)
214 }
215
216 return DotenvModule._configVault(options)
217}
218
219function decrypt (encrypted, keyStr) {
220 const key = Buffer.from(keyStr.slice(-64), 'hex')
221 let ciphertext = Buffer.from(encrypted, 'base64')
222
223 const nonce = ciphertext.slice(0, 12)
224 const authTag = ciphertext.slice(-16)
225 ciphertext = ciphertext.slice(12, -16)
226
227 try {
228 const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce)
229 aesgcm.setAuthTag(authTag)
230 return `${aesgcm.update(ciphertext)}${aesgcm.final()}`
231 } catch (error) {
232 const isRange = error instanceof RangeError
233 const invalidKeyLength = error.message === 'Invalid key length'
234 const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data'
235
236 if (isRange || invalidKeyLength) {
237 const msg = 'INVALID_DOTENV_KEY: It must be 64 characters long (or more)'
238 throw new Error(msg)
239 } else if (decryptionFailed) {
240 const msg = 'DECRYPTION_FAILED: Please check your DOTENV_KEY'
241 throw new Error(msg)
242 } else {
243 console.error('Error: ', error.code)
244 console.error('Error: ', error.message)
245 throw error
246 }
247 }
248}
249
250// Populate process.env with parsed values
251function populate (processEnv, parsed, options = {}) {
252 const debug = Boolean(options && options.debug)
253 const override = Boolean(options && options.override)
254
255 if (typeof parsed !== 'object') {
256 throw new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate')
257 }
258
259 // Set process.env
260 for (const key of Object.keys(parsed)) {
261 if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
262 if (override === true) {
263 processEnv[key] = parsed[key]
264 }
265
266 if (debug) {
267 if (override === true) {
268 _debug(`"${key}" is already defined and WAS overwritten`)
269 } else {
270 _debug(`"${key}" is already defined and was NOT overwritten`)
271 }
272 }
273 } else {
274 processEnv[key] = parsed[key]
275 }
276 }
277}
278
279const DotenvModule = {
280 configDotenv,
281 _configVault,
282 _parseVault,
283 config,
284 decrypt,
285 parse,
286 populate
287}
288
289module.exports.configDotenv = DotenvModule.configDotenv
290module.exports._configVault = DotenvModule._configVault
291module.exports._parseVault = DotenvModule._parseVault
292module.exports.config = DotenvModule.config
293module.exports.decrypt = DotenvModule.decrypt
294module.exports.parse = DotenvModule.parse
295module.exports.populate = DotenvModule.populate
296
297module.exports = DotenvModule