< Summary - Envilder CLI

Information
Class: src/envilder/core/domain/EnvironmentVariableName.ts
Assembly: Default
File(s): src/envilder/core/domain/EnvironmentVariableName.ts
Tag: 502_37137557711
Line coverage
100%
Covered lines: 9
Uncovered lines: 0
Coverable lines: 9
Total lines: 81
Line coverage: 100%
Branch coverage
100%
Covered branches: 9
Total branches: 9
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

src/envilder/core/domain/EnvironmentVariableName.ts

#LineLine coverage
 1/**
 2 * Pure domain rule describing which strings are usable as environment variable
 3 * names. Centralized here so every layer that accepts a name coming from a map
 4 * file or caller input (entity construction, mapping parsing, `.env` writing)
 5 * shares a single source of truth instead of duplicating the same regex and
 6 * message.
 7 *
 8 * The rule enforces one invariant: **an accepted name must survive a `.env`
 9 * round-trip as itself and nothing else.**
 10 *
 11 * That is expressed as an allowlist rather than a list of banned characters,
 12 * because a blocklist cannot hold the invariant. `.env` readers recognize a
 13 * key as `[A-Za-z0-9_.-]+`; anything outside that either makes the line
 14 * unparseable, so the variable is written and then silently lost (a space,
 15 * `#`, a tab, an accented or non-Latin letter, NUL, vertical tab), or is read
 16 * back as a *different* assignment, which is the injection in issue #511:
 17 *
 18 * - `=` opens a second assignment on the same line.
 19 * - CR and LF open a second line.
 20 * - U+2028 and U+2029 are JavaScript line terminators. The `.env` writer does
 21 *   not split on them, but `dotenv` parses lines with a multiline regex whose
 22 *   `^` matches after them, so a name embedding one is read back as a
 23 *   separate assignment.
 24 *
 25 * Dotted and hyphenated names (historically accepted, non-POSIX) stay valid:
 26 * they are inside the allowlist and do round-trip.
 27 */
 1628const READABLE_NAME = /^[A-Za-z0-9_.-]+$/;
 29
 30/**
 31 * `__proto__` is the one name inside the allowlist that still breaks the
 32 * round-trip. Writing it is fine, but every reader builds a plain object and
 33 * assigns into it, which routes the key through the `Object.prototype`
 34 * accessor instead of creating an own property -- `dotenv.parse` included, so
 35 * `dotenv.parse('__proto__=v')` returns `{}`. Accepting the name would mean
 36 * resolving the secret and writing a line that neither envilder nor any
 37 * dotenv-based consumer can read back, while reporting success.
 38 */
 1639const UNSUPPORTED_NAMES = new Set(['__proto__']);
 40
 41export function isValidEnvironmentVariableName(name: string): boolean {
 22442  return (
 43    typeof name === 'string' &&
 44    READABLE_NAME.test(name) &&
 45    !UNSUPPORTED_NAMES.has(name)
 46  );
 47}
 48
 49/**
 50 * Builds the rejection message, so every caller reports the same wording. The
 51 * name is quoted rather than interpolated: it is untrusted input, and quoting
 52 * keeps a name carrying line terminators from breaking the message across
 53 * lines. Only the name is echoed, never the mapped value.
 54 */
 55export function invalidEnvironmentVariableNameMessage(name: string): string {
 3856  if (typeof name !== 'string' || name.trim() === '') {
 857    return 'Environment variable name cannot be empty';
 58  }
 59
 3060  if (UNSUPPORTED_NAMES.has(name)) {
 161    return (
 62      `Unsupported environment variable name ${quoteName(name)}: ` +
 63      'a .env parser cannot read this name back, so it would be written ' +
 64      'but never resolved'
 65    );
 66  }
 67
 2968  return (
 69    `Invalid environment variable name ${quoteName(name)}: names may only ` +
 70    'contain letters, digits, underscore, dot and hyphen, so that they can ' +
 71    'be read back from a .env file'
 72  );
 73}
 74
 75function quoteName(name: string): string {
 76  // JSON.stringify escapes CR, LF and the other control characters, but leaves
 77  // U+2028/U+2029 literal even though they terminate a line in JavaScript.
 3078  return JSON.stringify(name)
 79    .replace(/\u{2028}/gu, '\\u2028')
 80    .replace(/\u{2029}/gu, '\\u2029');
 81}