@ix-xs/node-comfort 2.1.0 → 2.2.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 +153 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/Arr.js +13 -5
- package/src/Checker.js +56 -8
- package/src/Cli.js +60 -18
- package/src/Color.js +32 -6
- package/src/Crypto.js +429 -78
- package/src/Emitter.js +9 -9
- package/src/Env.js +90 -8
- package/src/FS.js +369 -59
- package/src/Func.js +1 -1
- package/src/Http.js +266 -75
- package/src/Id.js +53 -15
- package/src/Logger.js +100 -22
- package/src/Num.js +3 -2
- package/src/Obj.js +225 -36
- package/src/SQLite.js +155 -98
- package/src/Schema.js +281 -40
- package/src/Str.js +77 -29
- package/src/Sys.js +124 -23
- package/src/Time.js +98 -24
- package/src/Utils.js +18 -3
- package/src/errors.js +75 -4
- package/src/internal/shared.js +68 -14
- package/src/internal/terminal.js +49 -0
- package/types/index.d.ts +28 -5
- package/types/src/Arr.d.ts +4 -2
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Checker.d.ts +29 -3
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +6 -4
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +2 -2
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +126 -28
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +6 -6
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +19 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +30 -23
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +1 -1
- package/types/src/Http.d.ts +45 -8
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +11 -3
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +1 -1
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +15 -2
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +105 -13
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +61 -5
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +32 -7
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +43 -10
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +12 -7
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +16 -6
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +6 -2
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +7 -0
- package/types/src/internal/shared.d.ts.map +1 -1
- package/types/src/internal/terminal.d.ts +13 -0
- package/types/src/internal/terminal.d.ts.map +1 -0
package/src/Emitter.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const { TimeoutError, AbortError } = require("./errors.js");
|
|
18
|
-
const { toMs, setLongTimeout } = require("./internal/shared.js");
|
|
18
|
+
const { toMs, setLongTimeout, disposable } = require("./internal/shared.js");
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Event names mapped to their arguments, like
|
|
@@ -65,11 +65,11 @@ class Emitter {
|
|
|
65
65
|
* @param {keyof Events} event
|
|
66
66
|
* @param {_StoredListener} stored
|
|
67
67
|
* @param {ListenOptions | undefined} options
|
|
68
|
-
* @returns {() => void}
|
|
68
|
+
* @returns {(() => void) & Disposable}
|
|
69
69
|
*/
|
|
70
70
|
#add(event, stored, options) {
|
|
71
71
|
const signal = options?.signal;
|
|
72
|
-
if (signal?.aborted) return () => {};
|
|
72
|
+
if (signal?.aborted) return disposable(() => {});
|
|
73
73
|
const current = this.#listeners.get(event) ?? NONE;
|
|
74
74
|
if (!current.includes(stored)) this.#listeners.set(event, [...current, stored]);
|
|
75
75
|
const off = () => {
|
|
@@ -77,7 +77,7 @@ class Emitter {
|
|
|
77
77
|
this.#drop(event, stored);
|
|
78
78
|
};
|
|
79
79
|
signal?.addEventListener("abort", off, { once: true });
|
|
80
|
-
return off;
|
|
80
|
+
return disposable(off);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
@@ -109,7 +109,7 @@ class Emitter {
|
|
|
109
109
|
* @param {K} event
|
|
110
110
|
* @param {(...args: Events[K]) => unknown} listener
|
|
111
111
|
* @param {ListenOptions} [options]
|
|
112
|
-
* @returns {() => void} Removes the listener.
|
|
112
|
+
* @returns {(() => void) & Disposable} Removes the listener. With `using`, at the end of the block.
|
|
113
113
|
*/
|
|
114
114
|
on(event, listener, options) {
|
|
115
115
|
return this.#add(event, listener, options);
|
|
@@ -122,7 +122,7 @@ class Emitter {
|
|
|
122
122
|
* @param {K} event
|
|
123
123
|
* @param {(...args: Events[K]) => unknown} listener
|
|
124
124
|
* @param {ListenOptions} [options]
|
|
125
|
-
* @returns {() => void} Removes the listener if it hasn't run yet.
|
|
125
|
+
* @returns {(() => void) & Disposable} Removes the listener if it hasn't run yet. With `using`, at the end of the block.
|
|
126
126
|
*/
|
|
127
127
|
once(event, listener, options) {
|
|
128
128
|
/** @type {_StoredListener} */
|
|
@@ -143,13 +143,13 @@ class Emitter {
|
|
|
143
143
|
* emitter.onAny((event, ...args) => nc.debug(event, args));
|
|
144
144
|
*
|
|
145
145
|
* @param {(event: keyof Events & string, ...args: any[]) => unknown} listener
|
|
146
|
-
* @returns {() => void} Removes the listener.
|
|
146
|
+
* @returns {(() => void) & Disposable} Removes the listener. With `using`, at the end of the block.
|
|
147
147
|
*/
|
|
148
148
|
onAny(listener) {
|
|
149
149
|
if (!this.#any.includes(listener)) this.#any = [...this.#any, listener];
|
|
150
|
-
return () => {
|
|
150
|
+
return disposable(() => {
|
|
151
151
|
this.#any = this.#any.filter((stored) => stored !== listener);
|
|
152
|
-
};
|
|
152
|
+
});
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/**
|
package/src/Env.js
CHANGED
|
@@ -38,7 +38,7 @@ const { parseDuration } = require("./internal/shared.js");
|
|
|
38
38
|
* Options for `parse()`.
|
|
39
39
|
* @typedef {object} EnvParseOptions
|
|
40
40
|
* @property {boolean} [expand] Replace `$VAR`, `${VAR}` and `${VAR:-default}`. Defaults to `true`.
|
|
41
|
-
* @property {Record<string, string | undefined>} [env] Other variables usable in expansions. Defaults to `process.env
|
|
41
|
+
* @property {Record<string, string | undefined>} [env] Other variables usable in expansions. Defaults to none: a reference resolves only from the text itself, so parsing a file you don't trust can't copy your secrets into it. Pass `process.env` to read from it.
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -197,20 +197,27 @@ const _REFERENCE = /\\\$|\$\{([\w.]+)(?::?-([^}]*))?\}|\$([A-Za-z_]\w*)/g;
|
|
|
197
197
|
/** @type {Record<string, string>} */
|
|
198
198
|
const _ESCAPED = { n: "\n", r: "\r", t: "\t" };
|
|
199
199
|
|
|
200
|
+
/** How long one value may grow through `${VAR}` references. */
|
|
201
|
+
const _MAX_EXPANDED = 64 * 1024;
|
|
202
|
+
|
|
200
203
|
/**
|
|
201
204
|
* Parses the text of a `.env` file: quotes, multi-line values, `export`,
|
|
202
205
|
* comments, escapes, and `${VAR}` expansion (except in single quotes).
|
|
206
|
+
* References resolve from the text itself and from `env`, never from
|
|
207
|
+
* `process.env` unless you pass it.
|
|
203
208
|
*
|
|
204
209
|
* @example
|
|
205
210
|
* env.parse('PORT=3000\nURL="http://localhost:${PORT}"');
|
|
206
211
|
* // { PORT: "3000", URL: "http://localhost:3000" }
|
|
212
|
+
* env.parse("HOME_COPY=${HOME}", { env: process.env });
|
|
207
213
|
*
|
|
208
214
|
* @param {string} content
|
|
209
215
|
* @param {EnvParseOptions} [options]
|
|
210
216
|
* @returns {Record<string, string>}
|
|
217
|
+
* @throws {RangeError} If references make one value grow past 64 KB, as with variables that each use the one before several times.
|
|
211
218
|
*/
|
|
212
219
|
function parse(content, options = {}) {
|
|
213
|
-
return _parse(content, options);
|
|
220
|
+
return _parse(content, { ...options, env: options.env ?? {} });
|
|
214
221
|
}
|
|
215
222
|
|
|
216
223
|
/**
|
|
@@ -235,13 +242,18 @@ function _parse(content, options, wins) {
|
|
|
235
242
|
if (quote === "'") literal.add(key);
|
|
236
243
|
else literal.delete(key);
|
|
237
244
|
} else {
|
|
238
|
-
|
|
245
|
+
// One blank before the #, not a run of them: a long run would make the regex quadratic.
|
|
246
|
+
value = (bare ?? "").replace(/(?:^|[ \t])#.*$/, "").trim();
|
|
239
247
|
literal.delete(key);
|
|
240
248
|
}
|
|
241
249
|
out.set(key, value);
|
|
242
250
|
}
|
|
243
251
|
if (options.expand !== false) {
|
|
244
|
-
const base = options.env ??
|
|
252
|
+
const base = options.env ?? {};
|
|
253
|
+
// Each variable is expanded once: variables that repeat each other would
|
|
254
|
+
// otherwise take exponential time.
|
|
255
|
+
/** @type {Map<string, string>} */
|
|
256
|
+
const done = new Map();
|
|
245
257
|
/**
|
|
246
258
|
* @param {string} name
|
|
247
259
|
* @param {string[]} seen The variables being expanded, to stop at a loop.
|
|
@@ -251,8 +263,20 @@ function _parse(content, options, wins) {
|
|
|
251
263
|
if (seen.includes(name)) return "";
|
|
252
264
|
const winner = wins?.(name);
|
|
253
265
|
if (winner !== undefined) return winner;
|
|
254
|
-
|
|
255
|
-
|
|
266
|
+
return out.has(name) ? final(name, [...seen, name]) : _raw(base, name) ?? "";
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* @param {string} key
|
|
270
|
+
* @param {string[]} seen
|
|
271
|
+
* @returns {string}
|
|
272
|
+
*/
|
|
273
|
+
const final = (key, seen) => {
|
|
274
|
+
let value = done.get(key);
|
|
275
|
+
if (value === undefined) {
|
|
276
|
+
value = expand(/** @type {string} */ (out.get(key)), seen, key);
|
|
277
|
+
done.set(key, value);
|
|
278
|
+
}
|
|
279
|
+
return value;
|
|
256
280
|
};
|
|
257
281
|
/**
|
|
258
282
|
* @param {string} value
|
|
@@ -262,13 +286,19 @@ function _parse(content, options, wins) {
|
|
|
262
286
|
*/
|
|
263
287
|
const expand = (value, seen, key) => {
|
|
264
288
|
if (literal.has(key) || !value.includes("$")) return value;
|
|
289
|
+
const limit = Math.max(_MAX_EXPANDED, value.length);
|
|
290
|
+
let size = value.length;
|
|
265
291
|
return value.replace(_REFERENCE, (match, braced, fallback, plain) => {
|
|
266
292
|
if (match === "\\$") return "$";
|
|
267
293
|
const resolved = resolve(braced ?? plain, seen);
|
|
268
|
-
|
|
294
|
+
const result = resolved === "" && fallback !== undefined ? fallback : resolved;
|
|
295
|
+
size += result.length - match.length;
|
|
296
|
+
if (size > limit) throw new RangeError(`Environment variable ${key} expands to more than 64 KB: check for variables that each use the one before several times`);
|
|
297
|
+
return result;
|
|
269
298
|
});
|
|
270
299
|
};
|
|
271
|
-
for (const
|
|
300
|
+
for (const key of [...out.keys()]) final(key, [key]);
|
|
301
|
+
for (const [key, value] of done) out.set(key, value);
|
|
272
302
|
}
|
|
273
303
|
return Object.fromEntries(out);
|
|
274
304
|
}
|
|
@@ -282,6 +312,34 @@ function _parse(content, options, wins) {
|
|
|
282
312
|
const _automatic = new Map();
|
|
283
313
|
let _loadingAutomatically = false;
|
|
284
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Variables the automatic load never takes from a `.env` file: they run
|
|
317
|
+
* code in the programs started afterwards, or turn off TLS checks. A `.env`
|
|
318
|
+
* in a cloned repository is not to be trusted with those.
|
|
319
|
+
*/
|
|
320
|
+
const _DANGEROUS = new Set([
|
|
321
|
+
"NODE_OPTIONS", "NODE_TLS_REJECT_UNAUTHORIZED", "NODE_EXTRA_CA_CERTS", "NODE_PATH", "NODE_REDIRECT_WARNINGS", "NODE_REPL_EXTERNAL_MODULE",
|
|
322
|
+
"LD_PRELOAD", "LD_LIBRARY_PATH", "LD_AUDIT",
|
|
323
|
+
"NODE_USE_ENV_PROXY",
|
|
324
|
+
"GIT_SSH_COMMAND", "GIT_SSH", "GIT_ASKPASS", "GIT_EXEC_PATH", "GIT_CONFIG", "GIT_PROXY_COMMAND", "SSH_ASKPASS",
|
|
325
|
+
"GIT_EXTERNAL_DIFF", "GIT_TEMPLATE_DIR", "GIT_PAGER", "GIT_EDITOR", "GIT_SEQUENCE_EDITOR", "PAGER", "EDITOR", "VISUAL",
|
|
326
|
+
"PATH", "PATHEXT", "ComSpec", "COMSPEC", "BASH_ENV", "PROMPT_COMMAND",
|
|
327
|
+
"PERL5OPT", "PYTHONSTARTUP", "PYTHONPATH", "RUBYOPT", "JAVA_TOOL_OPTIONS", "_JAVA_OPTIONS",
|
|
328
|
+
]);
|
|
329
|
+
const _DANGEROUS_UPPER = new Set([..._DANGEROUS].map((name) => name.toUpperCase()));
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {string} key
|
|
333
|
+
* @returns {boolean}
|
|
334
|
+
*/
|
|
335
|
+
const _dangerous = (key) => {
|
|
336
|
+
// Windows reads Path and PATH as one variable.
|
|
337
|
+
const name = process.platform === "win32" ? key.toUpperCase() : key;
|
|
338
|
+
return (process.platform === "win32" ? _DANGEROUS_UPPER : _DANGEROUS).has(name) || name.startsWith("DYLD_") || name.startsWith("GIT_CONFIG_");
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const _WARNED = Symbol.for("@ix-xs/node-comfort/env-dangerous-warned");
|
|
342
|
+
|
|
285
343
|
/**
|
|
286
344
|
* The files that go with a `.env` file, most specific first.
|
|
287
345
|
* @param {string} file
|
|
@@ -296,6 +354,20 @@ const _cascade = (file, mode) => [`${file}.${mode}.local`, ...(mode === "test" ?
|
|
|
296
354
|
* `override`. The `.env` loaded by itself at startup doesn't count as
|
|
297
355
|
* already set: `.env.local` wins over it, as you'd expect.
|
|
298
356
|
*
|
|
357
|
+
* The load at startup leaves out the variables that can run code or weaken
|
|
358
|
+
* TLS, and says so on stderr: `NODE_OPTIONS`, `NODE_TLS_REJECT_UNAUTHORIZED`,
|
|
359
|
+
* `NODE_EXTRA_CA_CERTS`, `NODE_PATH`, `NODE_REDIRECT_WARNINGS`,
|
|
360
|
+
* `NODE_REPL_EXTERNAL_MODULE`, `NODE_USE_ENV_PROXY`, `LD_PRELOAD`,
|
|
361
|
+
* `LD_LIBRARY_PATH`, `LD_AUDIT`,
|
|
362
|
+
* `DYLD_*`, `GIT_SSH_COMMAND`, `GIT_SSH`, `GIT_ASKPASS`, `GIT_EXEC_PATH`,
|
|
363
|
+
* `GIT_CONFIG*`, `GIT_PROXY_COMMAND`, `GIT_EXTERNAL_DIFF`, `GIT_TEMPLATE_DIR`,
|
|
364
|
+
* `GIT_PAGER`, `GIT_EDITOR`, `GIT_SEQUENCE_EDITOR`, `PAGER`, `EDITOR`,
|
|
365
|
+
* `VISUAL`, `SSH_ASKPASS`, `PATH`, `PATHEXT`,
|
|
366
|
+
* `ComSpec`, `BASH_ENV`, `PROMPT_COMMAND`, `PERL5OPT`,
|
|
367
|
+
* `PYTHONSTARTUP`, `PYTHONPATH`, `RUBYOPT`, `JAVA_TOOL_OPTIONS` and
|
|
368
|
+
* `_JAVA_OPTIONS`. A call to `load()` loads them all: don't make one on a
|
|
369
|
+
* file you don't trust, such as the `.env` of a repository someone sent you.
|
|
370
|
+
*
|
|
299
371
|
* @example
|
|
300
372
|
* env.load(); // ./.env
|
|
301
373
|
* env.load([".env.local", ".env"]); // local values win
|
|
@@ -344,13 +416,23 @@ function load(files = ".env", options = {}) {
|
|
|
344
416
|
}
|
|
345
417
|
}
|
|
346
418
|
}
|
|
419
|
+
/** @type {string[]} */
|
|
420
|
+
const skipped = [];
|
|
347
421
|
for (const [key, value] of loaded) {
|
|
348
422
|
if (kept(key) !== undefined) continue;
|
|
423
|
+
if (own && _loadingAutomatically && _dangerous(key)) {
|
|
424
|
+
skipped.push(key);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
349
427
|
target[key] = value;
|
|
350
428
|
if (!own) continue;
|
|
351
429
|
if (_loadingAutomatically) _automatic.set(key, value);
|
|
352
430
|
else _automatic.delete(key);
|
|
353
431
|
}
|
|
432
|
+
if (skipped.length && !(/** @type {any} */ (process)[_WARNED])) {
|
|
433
|
+
/** @type {any} */ (process)[_WARNED] = true;
|
|
434
|
+
process.stderr.write(`[@ix-xs/node-comfort] Left out of the automatic .env load, as they can run code or weaken TLS: ${skipped.join(", ")}. Call env.load() to load them on purpose.\n`);
|
|
435
|
+
}
|
|
354
436
|
return Object.fromEntries(loaded);
|
|
355
437
|
}
|
|
356
438
|
|