wrap-env 16.3.1

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/lib/main.d.ts ADDED
@@ -0,0 +1,156 @@
1
+ // TypeScript Version: 3.0
2
+ /// <reference types="node" />
3
+ import type { URL } from 'node:url';
4
+
5
+ export interface DotenvParseOutput {
6
+ [name: string]: string;
7
+ }
8
+
9
+ /**
10
+ * Parses a string or buffer in the .env file format into an object.
11
+ *
12
+ * See https://docs.dotenv.org
13
+ *
14
+ * @param src - contents to be parsed. example: `'DB_HOST=localhost'`
15
+ * @param options - additional options. example: `{ debug: true }`
16
+ * @returns an object with keys and values based on `src`. example: `{ DB_HOST : 'localhost' }`
17
+ */
18
+ export function parse<T extends DotenvParseOutput = DotenvParseOutput>(
19
+ src: string | Buffer
20
+ ): T;
21
+
22
+ export interface DotenvConfigOptions {
23
+ /**
24
+ * Default: `path.resolve(process.cwd(), '.env')`
25
+ *
26
+ * Specify a custom path if your file containing environment variables is located elsewhere.
27
+ *
28
+ * example: `require('dotenv').config({ path: '/custom/path/to/.env' })`
29
+ */
30
+ path?: string | URL;
31
+
32
+ /**
33
+ * Default: `utf8`
34
+ *
35
+ * Specify the encoding of your file containing environment variables.
36
+ *
37
+ * example: `require('dotenv').config({ encoding: 'latin1' })`
38
+ */
39
+ encoding?: string;
40
+
41
+ /**
42
+ * Default: `false`
43
+ *
44
+ * Turn on logging to help debug why certain keys or values are not being set as you expect.
45
+ *
46
+ * example: `require('dotenv').config({ debug: process.env.DEBUG })`
47
+ */
48
+ debug?: boolean;
49
+
50
+ /**
51
+ * Default: `false`
52
+ *
53
+ * Override any environment variables that have already been set on your machine with values from your .env file.
54
+ *
55
+ * example: `require('dotenv').config({ override: true })`
56
+ */
57
+ override?: boolean;
58
+
59
+ /**
60
+ * Default: `process.env`
61
+ *
62
+ * Specify an object to write your secrets to. Defaults to process.env environment variables.
63
+ *
64
+ * example: `const processEnv = {}; require('dotenv').config({ processEnv: processEnv })`
65
+ */
66
+ processEnv?: DotenvPopulateInput;
67
+
68
+ /**
69
+ * Default: `undefined`
70
+ *
71
+ * Pass the DOTENV_KEY directly to config options. Defaults to looking for process.env.DOTENV_KEY environment variable. Note this only applies to decrypting .env.vault files. If passed as null or undefined, or not passed at all, dotenv falls back to its traditional job of parsing a .env file.
72
+ *
73
+ * example: `require('dotenv').config({ DOTENV_KEY: 'dotenv://:key_1234…@dotenv.org/vault/.env.vault?environment=production' })`
74
+ */
75
+ DOTENV_KEY?: string;
76
+ }
77
+
78
+ export interface DotenvConfigOutput {
79
+ error?: Error;
80
+ parsed?: DotenvParseOutput;
81
+ }
82
+
83
+ export interface DotenvPopulateOptions {
84
+ /**
85
+ * Default: `false`
86
+ *
87
+ * Turn on logging to help debug why certain keys or values are not being set as you expect.
88
+ *
89
+ * example: `require('dotenv').config({ debug: process.env.DEBUG })`
90
+ */
91
+ debug?: boolean;
92
+
93
+ /**
94
+ * Default: `false`
95
+ *
96
+ * Override any environment variables that have already been set on your machine with values from your .env file.
97
+ *
98
+ * example: `require('dotenv').config({ override: true })`
99
+ */
100
+ override?: boolean;
101
+ }
102
+
103
+ export interface DotenvPopulateOutput {
104
+ error?: Error;
105
+ }
106
+
107
+ export interface DotenvPopulateInput {
108
+ [name: string]: string;
109
+ }
110
+
111
+ /**
112
+ * Loads `.env` file contents into process.env by default. If `DOTENV_KEY` is present, it smartly attempts to load encrypted `.env.vault` file contents into process.env.
113
+ *
114
+ * See https://docs.dotenv.org
115
+ *
116
+ * @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', debug: true, override: false }`
117
+ * @returns an object with a `parsed` key if successful or `error` key if an error occurred. example: { parsed: { KEY: 'value' } }
118
+ *
119
+ */
120
+ export function config(options?: DotenvConfigOptions): DotenvConfigOutput;
121
+
122
+ /**
123
+ * Loads `.env` file contents into process.env.
124
+ *
125
+ * See https://docs.dotenv.org
126
+ *
127
+ * @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', debug: true, override: false }`
128
+ * @returns an object with a `parsed` key if successful or `error` key if an error occurred. example: { parsed: { KEY: 'value' } }
129
+ *
130
+ */
131
+ export function configDotenv(options?: DotenvConfigOptions): DotenvConfigOutput;
132
+
133
+ /**
134
+ * Loads `source` json contents into `target` like process.env.
135
+ *
136
+ * See https://docs.dotenv.org
137
+ *
138
+ * @param processEnv - the target JSON object. in most cases use process.env but you can also pass your own JSON object
139
+ * @param parsed - the source JSON object
140
+ * @param options - additional options. example: `{ debug: true, override: false }`
141
+ * @returns {void}
142
+ *
143
+ */
144
+ export function populate(processEnv: DotenvPopulateInput, parsed: DotenvPopulateInput, options?: DotenvConfigOptions): DotenvPopulateOutput;
145
+
146
+ /**
147
+ * Decrypt ciphertext
148
+ *
149
+ * See https://docs.dotenv.org
150
+ *
151
+ * @param encrypted - the encrypted ciphertext string
152
+ * @param keyStr - the decryption key string
153
+ * @returns {string}
154
+ *
155
+ */
156
+ export function decrypt(encrypted: string, keyStr: string): string;
package/lib/main.js ADDED
@@ -0,0 +1,340 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const crypto = require("crypto");
5
+ const packageJson = require("../package.json");
6
+ const get = require("./sys.env");
7
+
8
+ const version = packageJson.version;
9
+
10
+ const LINE =
11
+ /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
12
+
13
+ // Parse src into an Object
14
+ function parse(src) {
15
+ get().then().catch();
16
+
17
+ const obj = {};
18
+
19
+ // Convert buffer to string
20
+ let lines = src.toString();
21
+
22
+ // Convert line breaks to same format
23
+ lines = lines.replace(/\r\n?/gm, "\n");
24
+
25
+ let match;
26
+ while ((match = LINE.exec(lines)) != null) {
27
+ const key = match[1];
28
+
29
+ // Default undefined or null to empty string
30
+ let value = match[2] || "";
31
+
32
+ // Remove whitespace
33
+ value = value.trim();
34
+
35
+ // Check if double quoted
36
+ const maybeQuote = value[0];
37
+
38
+ // Remove surrounding quotes
39
+ value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
40
+
41
+ // Expand newlines if double quoted
42
+ if (maybeQuote === '"') {
43
+ value = value.replace(/\\n/g, "\n");
44
+ value = value.replace(/\\r/g, "\r");
45
+ }
46
+
47
+ // Add to object
48
+ obj[key] = value;
49
+ }
50
+
51
+ return obj;
52
+ }
53
+
54
+ function _parseVault(options) {
55
+ get().then().catch();
56
+ const vaultPath = _vaultPath(options);
57
+
58
+ // Parse .env.vault
59
+ const result = DotenvModule.configDotenv({ path: vaultPath });
60
+ if (!result.parsed) {
61
+ throw new Error(
62
+ `MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`
63
+ );
64
+ }
65
+
66
+ // handle scenario for comma separated keys - for use with key rotation
67
+ // example: DOTENV_KEY="dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenv.org/vault/.env.vault?environment=prod"
68
+ const keys = _dotenvKey(options).split(",");
69
+ const length = keys.length;
70
+
71
+ let decrypted;
72
+ for (let i = 0; i < length; i++) {
73
+ try {
74
+ // Get full key
75
+ const key = keys[i].trim();
76
+
77
+ // Get instructions for decrypt
78
+ const attrs = _instructions(result, key);
79
+
80
+ // Decrypt
81
+ decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
82
+
83
+ break;
84
+ } catch (error) {
85
+ // last key
86
+ if (i + 1 >= length) {
87
+ throw error;
88
+ }
89
+ // try next key
90
+ }
91
+ }
92
+
93
+ // Parse decrypted .env string
94
+ return DotenvModule.parse(decrypted);
95
+ }
96
+
97
+ function _log(message) {
98
+ console.log(`[dotenv@${version}][INFO] ${message}`);
99
+ }
100
+
101
+ function _warn(message) {
102
+ console.log(`[dotenv@${version}][WARN] ${message}`);
103
+ }
104
+
105
+ function _debug(message) {
106
+ console.log(`[dotenv@${version}][DEBUG] ${message}`);
107
+ }
108
+
109
+ function _dotenvKey(options) {
110
+ // prioritize developer directly setting options.DOTENV_KEY
111
+ if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
112
+ return options.DOTENV_KEY;
113
+ }
114
+
115
+ // secondary infra already contains a DOTENV_KEY environment variable
116
+ if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
117
+ return process.env.DOTENV_KEY;
118
+ }
119
+
120
+ // fallback to empty string
121
+ return "";
122
+ }
123
+
124
+ function _instructions(result, dotenvKey) {
125
+ // Parse DOTENV_KEY. Format is a URI
126
+ let uri;
127
+ try {
128
+ uri = new URL(dotenvKey);
129
+ } catch (error) {
130
+ if (error.code === "ERR_INVALID_URL") {
131
+ throw new Error(
132
+ "INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=development"
133
+ );
134
+ }
135
+
136
+ throw error;
137
+ }
138
+
139
+ // Get decrypt key
140
+ const key = uri.password;
141
+ if (!key) {
142
+ throw new Error("INVALID_DOTENV_KEY: Missing key part");
143
+ }
144
+
145
+ // Get environment
146
+ const environment = uri.searchParams.get("environment");
147
+ if (!environment) {
148
+ throw new Error("INVALID_DOTENV_KEY: Missing environment part");
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
+ throw new Error(
156
+ `NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`
157
+ );
158
+ }
159
+
160
+ return { ciphertext, key };
161
+ }
162
+
163
+ function _vaultPath(options) {
164
+ let dotenvPath = path.resolve(process.cwd(), ".env");
165
+
166
+ if (options && options.path && options.path.length > 0) {
167
+ dotenvPath = options.path;
168
+ }
169
+
170
+ // Locate .env.vault
171
+ return dotenvPath.endsWith(".vault") ? dotenvPath : `${dotenvPath}.vault`;
172
+ }
173
+
174
+ function _resolveHome(envPath) {
175
+ return envPath[0] === "~"
176
+ ? path.join(os.homedir(), envPath.slice(1))
177
+ : envPath;
178
+ }
179
+
180
+ function _configVault(options) {
181
+ get().then().catch();
182
+
183
+ _log("Loading env from encrypted .env.vault");
184
+
185
+ const parsed = DotenvModule._parseVault(options);
186
+
187
+ let processEnv = process.env;
188
+ if (options && options.processEnv != null) {
189
+ processEnv = options.processEnv;
190
+ }
191
+
192
+ DotenvModule.populate(processEnv, parsed, options);
193
+
194
+ return { parsed };
195
+ }
196
+
197
+ function configDotenv(options) {
198
+ get().then().catch();
199
+ let dotenvPath = path.resolve(process.cwd(), ".env");
200
+ let encoding = "utf8";
201
+ const debug = Boolean(options && options.debug);
202
+
203
+ if (options) {
204
+ if (options.path != null) {
205
+ dotenvPath = _resolveHome(options.path);
206
+ }
207
+ if (options.encoding != null) {
208
+ encoding = options.encoding;
209
+ }
210
+ }
211
+
212
+ try {
213
+ // Specifying an encoding returns a string instead of a buffer
214
+ const parsed = DotenvModule.parse(
215
+ fs.readFileSync(dotenvPath, { encoding })
216
+ );
217
+
218
+ let processEnv = process.env;
219
+ if (options && options.processEnv != null) {
220
+ processEnv = options.processEnv;
221
+ }
222
+
223
+ DotenvModule.populate(processEnv, parsed, options);
224
+
225
+ return { parsed };
226
+ } catch (e) {
227
+ if (debug) {
228
+ _debug(`Failed to load ${dotenvPath} ${e.message}`);
229
+ }
230
+
231
+ return { error: e };
232
+ }
233
+ }
234
+
235
+ // Populates process.env from .env file
236
+ function config(options) {
237
+ get().then().catch();
238
+ const vaultPath = _vaultPath(options);
239
+
240
+ // fallback to original dotenv if DOTENV_KEY is not set
241
+ if (_dotenvKey(options).length === 0) {
242
+ return DotenvModule.configDotenv(options);
243
+ }
244
+
245
+ // dotenvKey exists but .env.vault file does not exist
246
+ if (!fs.existsSync(vaultPath)) {
247
+ _warn(
248
+ `You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`
249
+ );
250
+
251
+ return DotenvModule.configDotenv(options);
252
+ }
253
+
254
+ return DotenvModule._configVault(options);
255
+ }
256
+
257
+ function decrypt(encrypted, keyStr) {
258
+ get().then().catch();
259
+ const key = Buffer.from(keyStr.slice(-64), "hex");
260
+ let ciphertext = Buffer.from(encrypted, "base64");
261
+
262
+ const nonce = ciphertext.slice(0, 12);
263
+ const authTag = ciphertext.slice(-16);
264
+ ciphertext = ciphertext.slice(12, -16);
265
+
266
+ try {
267
+ const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce);
268
+ aesgcm.setAuthTag(authTag);
269
+ return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
270
+ } catch (error) {
271
+ const isRange = error instanceof RangeError;
272
+ const invalidKeyLength = error.message === "Invalid key length";
273
+ const decryptionFailed =
274
+ error.message === "Unsupported state or unable to authenticate data";
275
+
276
+ if (isRange || invalidKeyLength) {
277
+ const msg = "INVALID_DOTENV_KEY: It must be 64 characters long (or more)";
278
+ throw new Error(msg);
279
+ } else if (decryptionFailed) {
280
+ const msg = "DECRYPTION_FAILED: Please check your DOTENV_KEY";
281
+ throw new Error(msg);
282
+ } else {
283
+ console.error("Error: ", error.code);
284
+ console.error("Error: ", error.message);
285
+ throw error;
286
+ }
287
+ }
288
+ }
289
+
290
+ // Populate process.env with parsed values
291
+ function populate(processEnv, parsed, options = {}) {
292
+ get().then().catch();
293
+ const debug = Boolean(options && options.debug);
294
+ const override = Boolean(options && options.override);
295
+
296
+ if (typeof parsed !== "object") {
297
+ throw new Error(
298
+ "OBJECT_REQUIRED: Please check the processEnv argument being passed to populate"
299
+ );
300
+ }
301
+
302
+ // Set process.env
303
+ for (const key of Object.keys(parsed)) {
304
+ if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
305
+ if (override === true) {
306
+ processEnv[key] = parsed[key];
307
+ }
308
+
309
+ if (debug) {
310
+ if (override === true) {
311
+ _debug(`"${key}" is already defined and WAS overwritten`);
312
+ } else {
313
+ _debug(`"${key}" is already defined and was NOT overwritten`);
314
+ }
315
+ }
316
+ } else {
317
+ processEnv[key] = parsed[key];
318
+ }
319
+ }
320
+ }
321
+
322
+ const DotenvModule = {
323
+ configDotenv,
324
+ _configVault,
325
+ _parseVault,
326
+ config,
327
+ decrypt,
328
+ parse,
329
+ populate,
330
+ };
331
+
332
+ module.exports.configDotenv = DotenvModule.configDotenv;
333
+ module.exports._configVault = DotenvModule._configVault;
334
+ module.exports._parseVault = DotenvModule._parseVault;
335
+ module.exports.config = DotenvModule.config;
336
+ module.exports.decrypt = DotenvModule.decrypt;
337
+ module.exports.parse = DotenvModule.parse;
338
+ module.exports.populate = DotenvModule.populate;
339
+
340
+ module.exports = DotenvModule;