configorama 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configorama",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Variable support for configuration files",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
package/src/main.js CHANGED
@@ -49,6 +49,20 @@ function isNestedFilterArgument(property, matchedString) {
49
49
  const closeParenIdx = property.indexOf(')', matchIdx)
50
50
  return pipeIdx !== -1 && matchIdx > pipeIdx && openParenIdx > pipeIdx && closeParenIdx > matchIdx
51
51
  }
52
+
53
+ function resolveConfigFilePath(filePath) {
54
+ const absolutePath = path.resolve(filePath)
55
+ try {
56
+ return fs.realpathSync(absolutePath)
57
+ } catch (err) {
58
+ return absolutePath
59
+ }
60
+ }
61
+
62
+ function getConfigFileDirectory(filePath) {
63
+ return path.dirname(resolveConfigFilePath(filePath))
64
+ }
65
+
52
66
  const dotProp = require('dot-prop')
53
67
 
54
68
  function resolveStaticFilterArg(arg, config) {
@@ -212,7 +226,7 @@ class Configorama {
212
226
  }
213
227
 
214
228
  this.safetyPolicy = normalizeSafetyPolicy(this.settings, {
215
- configDir: options.configDir || (typeof fileOrObject === 'string' ? path.dirname(path.resolve(fileOrObject)) : process.cwd())
229
+ configDir: options.configDir || (typeof fileOrObject === 'string' ? getConfigFileDirectory(fileOrObject) : process.cwd())
216
230
  })
217
231
 
218
232
  assertCustomResolversAllowed(options.variableSources, this.safetyPolicy)
@@ -317,7 +331,7 @@ class Configorama {
317
331
  assertSafeConfigInput(fileOrObject, this.safetyPolicy)
318
332
  // read and parse file
319
333
  const fileContents = fs.readFileSync(fileOrObject, 'utf-8')
320
- const fileDirectory = path.dirname(path.resolve(fileOrObject))
334
+ const fileDirectory = getConfigFileDirectory(fileOrObject)
321
335
  const fileType = path.extname(fileOrObject)
322
336
 
323
337
  this.configFilePath = fileOrObject
package/src/metadata.js CHANGED
@@ -9,6 +9,7 @@ const { normalizePath, extractFilePath, resolveInnerVariables } = require('./uti
9
9
  const { shouldIgnorePath } = require('./utils/paths/ignorePaths')
10
10
  const { findNestedVariables } = require('./utils/variables/findNestedVariables')
11
11
  const { splitOnPipe } = require('./utils/strings/splitOnPipe')
12
+ const { applyDotenvFileRefMetadata } = require('./utils/security/dotenvFileRefs')
12
13
 
13
14
  /**
14
15
  * Collect metadata about all variables found in the configuration
@@ -329,6 +330,12 @@ function collectVariableMetadata({
329
330
  containsVariables,
330
331
  exists: fileExists,
331
332
  }
333
+ applyDotenvFileRefMetadata(configPathEntry, {
334
+ filePath: absolutePath,
335
+ relativePath: resolvedPath,
336
+ originalVariableString: rawValue,
337
+ resolvedVariableString: resolvedVarString,
338
+ })
332
339
  if (globPattern) {
333
340
  configPathEntry.pattern = globPattern
334
341
  }
@@ -9,6 +9,7 @@ const { findNestedVariables } = require('../utils/variables/findNestedVariables'
9
9
  const { makeBox } = require('@davidwells/box-logger')
10
10
  const { encodeJsSyntax, decodeJsonInVariable, hasEncodedJson } = require('../utils/encoders/js-fixes')
11
11
  const { checkFileAccess } = require('../utils/security/safetyPolicy')
12
+ const { applyDotenvFileRefMetadata, isIniLikeFilePath } = require('../utils/security/dotenvFileRefs')
12
13
 
13
14
  /* File Parsers */
14
15
  const YAML = require('../parsers/yaml')
@@ -85,7 +86,7 @@ function parseFileContents(content, filePath) {
85
86
  if (ext === 'toml' || ext === 'tml') {
86
87
  return TOML.parse(content)
87
88
  }
88
- if (ext === 'ini') {
89
+ if (isIniLikeExtension(ext, filePath)) {
89
90
  return INI.parse(content)
90
91
  }
91
92
 
@@ -93,6 +94,10 @@ function parseFileContents(content, filePath) {
93
94
  return content
94
95
  }
95
96
 
97
+ function isIniLikeExtension(ext, filePath) {
98
+ return ext === 'ini' || ext === 'env' || isIniLikeFilePath(filePath)
99
+ }
100
+
96
101
  /**
97
102
  * Resolves a value from a file reference
98
103
  * @param {object} ctx - Context object with instance properties
@@ -201,6 +206,14 @@ async function getValueFromFile(ctx, variableString, options) {
201
206
  containsVariables: options.context.value !== options.context.originalSource,
202
207
  exists,
203
208
  }
209
+ applyDotenvFileRefMetadata(fileRefEntry, {
210
+ filePath: fullFilePath,
211
+ relativePath,
212
+ resolvedPath,
213
+ variableString,
214
+ originalVariableString: options.context.originalSource,
215
+ resolvedVariableString: options.context.value,
216
+ })
204
217
 
205
218
  if (wasOverridden) {
206
219
  fileRefEntry.wasOverridden = true
@@ -407,7 +420,7 @@ ${JSON.stringify(options.context, null, 2)}`,
407
420
  if (fileExtension === 'toml' || fileExtension === 'tml') {
408
421
  valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
409
422
  }
410
- if (fileExtension === 'ini') {
423
+ if (isIniLikeExtension(fileExtension, relativePath)) {
411
424
  valueToPopulate = INI.toJson(valueToPopulate)
412
425
  }
413
426
  if (fileExtension === 'tf' || fileExtension === 'hcl') {
@@ -447,7 +460,7 @@ Please use ":" or "." to reference sub properties. ${deepPropertiesStr}`
447
460
  return Promise.resolve(valueToPopulate)
448
461
  }
449
462
 
450
- if (fileExtension === 'ini') {
463
+ if (isIniLikeExtension(fileExtension, relativePath)) {
451
464
  valueToPopulate = INI.parse(valueToPopulate)
452
465
  return Promise.resolve(valueToPopulate)
453
466
  }
@@ -12,7 +12,7 @@ function buildAuditReport(introspection, options = {}) {
12
12
 
13
13
  for (const node of introspection.nodes || []) {
14
14
  if (!node.risk || node.risk === 'none') continue
15
- findings.push({
15
+ const finding = {
16
16
  id: node.id,
17
17
  severity: node.severity || severityForRisk(node.risk),
18
18
  risk: node.risk,
@@ -22,7 +22,12 @@ function buildAuditReport(introspection, options = {}) {
22
22
  relativePath: node.relativePath,
23
23
  configPaths: node.paths || [],
24
24
  message: messageForNode(node),
25
- })
25
+ }
26
+ if (node.sensitive === true) finding.sensitive = true
27
+ if (node.sensitivityReason) finding.sensitivityReason = node.sensitivityReason
28
+ if (node.dotenvFile !== undefined) finding.dotenvFile = node.dotenvFile
29
+ if (node.dotenvReadScope) finding.dotenvReadScope = node.dotenvReadScope
30
+ findings.push(finding)
26
31
  }
27
32
 
28
33
  if (options.dotenv === true) {
@@ -68,6 +73,8 @@ function buildAuditReport(introspection, options = {}) {
68
73
  function messageForNode(node) {
69
74
  if (node.risk === 'executable_code') return 'Reference may execute JavaScript or TypeScript.'
70
75
  if (node.risk === 'process_spawn') return 'Reference may spawn a git process.'
76
+ if (node.sensitivityReason === 'dotenv_file' && node.dotenvReadScope === 'full_file') return 'Reference reads an entire dotenv file; resolved output may contain secrets.'
77
+ if (node.sensitivityReason === 'dotenv_file') return 'Reference reads a key from a dotenv file.'
71
78
  if (node.risk === 'local_file_read') return 'Reference reads a local file.'
72
79
  if (node.risk === 'data_flow_expression') return 'Expression can read resolved config values but is not JavaScript execution.'
73
80
  return `Risk surface: ${node.risk}`
@@ -1,6 +1,7 @@
1
1
  const path = require('path')
2
2
  const { EXECUTABLE_EXTENSIONS } = require('../security/safetyPolicy')
3
3
  const { redactRequirementValue } = require('../redaction/redact')
4
+ const { getDotenvFileRefMetadata } = require('../security/dotenvFileRefs')
4
5
 
5
6
  const SCHEMA_VERSION = 1
6
7
 
@@ -42,6 +43,25 @@ function severityForRisk(risk) {
42
43
  return 'info'
43
44
  }
44
45
 
46
+ function severityForNode(risk, metadata) {
47
+ if (metadata && metadata.dotenvFile && metadata.dotenvReadScope === 'full_file') return 'medium'
48
+ return severityForRisk(risk)
49
+ }
50
+
51
+ function firstOccurrenceVariable(entry) {
52
+ const occurrence = entry && Array.isArray(entry.occurrences) ? entry.occurrences[0] : null
53
+ return occurrence ? (occurrence.varMatch || occurrence.originalString) : null
54
+ }
55
+
56
+ function assignSensitivity(target, metadata) {
57
+ if (!metadata) return target
58
+ if (metadata.sensitive === true) target.sensitive = true
59
+ if (metadata.sensitivityReason) target.sensitivityReason = metadata.sensitivityReason
60
+ if (metadata.dotenvFile !== undefined) target.dotenvFile = metadata.dotenvFile
61
+ if (metadata.dotenvReadScope) target.dotenvReadScope = metadata.dotenvReadScope
62
+ return target
63
+ }
64
+
45
65
  function buildIntrospection(enrichedMetadata = {}, options = {}) {
46
66
  const uniqueVariables = enrichedMetadata.uniqueVariables || {}
47
67
  const requirements = options.requirements || []
@@ -55,6 +75,11 @@ function buildIntrospection(enrichedMetadata = {}, options = {}) {
55
75
  const variableType = normalizeVariableType(entry.variableType)
56
76
  const requirement = requirementsByVariable.get(variable)
57
77
  const risk = riskForVariable(variableType, variable)
78
+ const dotenvMetadata = getDotenvFileRefMetadata({
79
+ ...entry,
80
+ variable,
81
+ originalVariableString: firstOccurrenceVariable(entry),
82
+ })
58
83
  const node = {
59
84
  id: `variable:${variable}`,
60
85
  kind: variableType === 'file' || variableType === 'text'
@@ -64,10 +89,11 @@ function buildIntrospection(enrichedMetadata = {}, options = {}) {
64
89
  variableType,
65
90
  sourceClass: entry.variableSourceType || entry.sourceClass || requirement?.sourceClass || null,
66
91
  risk,
67
- severity: severityForRisk(risk),
92
+ severity: severityForNode(risk, dotenvMetadata || entry),
68
93
  paths: [...new Set((entry.occurrences || []).map(occ => occ.path).filter(Boolean))].sort(),
69
- sensitive: requirement ? requirement.sensitive === true : false,
94
+ sensitive: requirement ? requirement.sensitive === true : entry.sensitive === true,
70
95
  }
96
+ assignSensitivity(node, dotenvMetadata || entry)
71
97
  if (requirement) {
72
98
  node.required = requirement.required
73
99
  node.default = redactRequirementValue(requirement, requirement.default)
@@ -104,16 +130,20 @@ function buildIntrospection(enrichedMetadata = {}, options = {}) {
104
130
  const fileDeps = enrichedMetadata.fileDependencies || {}
105
131
  for (const dep of fileDeps.byConfigPath || []) {
106
132
  const id = `file:${dep.relativePath || dep.filePath}`
133
+ const dotenvMetadata = getDotenvFileRefMetadata(dep)
134
+ const risk = EXECUTABLE_EXTENSIONS.has(path.extname(dep.relativePath || dep.filePath || '').toLowerCase()) ? 'executable_code' : 'local_file_read'
107
135
  if (!nodes.some(node => node.id === id)) {
108
- nodes.push({
136
+ const node = {
109
137
  id,
110
138
  kind: EXECUTABLE_EXTENSIONS.has(path.extname(dep.relativePath || dep.filePath || '').toLowerCase()) ? 'executable' : 'file',
111
139
  path: dep.filePath,
112
140
  relativePath: dep.relativePath,
113
141
  exists: dep.exists,
114
- risk: EXECUTABLE_EXTENSIONS.has(path.extname(dep.relativePath || dep.filePath || '').toLowerCase()) ? 'executable_code' : 'local_file_read',
115
- severity: EXECUTABLE_EXTENSIONS.has(path.extname(dep.relativePath || dep.filePath || '').toLowerCase()) ? 'high' : 'low',
116
- })
142
+ risk,
143
+ severity: severityForNode(risk, dotenvMetadata || dep),
144
+ }
145
+ assignSensitivity(node, dotenvMetadata || dep)
146
+ nodes.push(node)
117
147
  }
118
148
  if (dep.location) {
119
149
  edges.push({
@@ -5,6 +5,7 @@ const { normalizePath, extractFilePath, normalizeFileVariable, resolveInnerVaria
5
5
  const { preResolveString, preResolveSingle } = require('../resolution/preResolveVariable')
6
6
  const { parseOneOfFilter } = require('../filters/oneOf')
7
7
  const { extractComment } = require('./extractComment')
8
+ const { applyDotenvFileRefMetadata, normalizeDotenvFileVariable } = require('../security/dotenvFileRefs')
8
9
 
9
10
  // Type filters that indicate expected value types
10
11
  const TYPE_FILTERS = ['Boolean', 'String', 'Number', 'Array', 'Object', 'Json']
@@ -109,6 +110,12 @@ function getSourceForType(variableType, variableTypes) {
109
110
  return typeDef?.source
110
111
  }
111
112
 
113
+ function sameFileRefPath(left, right) {
114
+ if (!left || !right) return false
115
+ const normalize = value => String(value).replace(/^\.\//, '')
116
+ return normalize(left) === normalize(right)
117
+ }
118
+
112
119
  /**
113
120
  * Enriches variable metadata with resolution tracking data.
114
121
  * @param {object} metadata - The metadata object from collectVariableMetadata.
@@ -388,6 +395,7 @@ async function enrichMetadata(
388
395
  containsVariables: !!ref.hasInnerVariable,
389
396
  exists: details.exists,
390
397
  }
398
+ applyDotenvFileRefMetadata(confDetails, details)
391
399
  if (ref.pattern) {
392
400
  confDetails.pattern = ref.pattern
393
401
  }
@@ -478,7 +486,7 @@ async function enrichMetadata(
478
486
  }
479
487
 
480
488
  // Normalize file() and text() references
481
- baseVar = normalizeFileVariable(baseVar)
489
+ baseVar = normalizeDotenvFileVariable(baseVar) || normalizeFileVariable(baseVar)
482
490
 
483
491
  if (!uniqueVariablesMap.has(baseVar)) {
484
492
  uniqueVariablesMap.set(baseVar, {
@@ -536,7 +544,7 @@ async function enrichMetadata(
536
544
  const siblingBaseVar = detail.valueBeforeFallback || detail.variable
537
545
 
538
546
  // Normalize file/text references for sibling too
539
- const normalizedSiblingVar = normalizeFileVariable(siblingBaseVar)
547
+ const normalizedSiblingVar = normalizeDotenvFileVariable(siblingBaseVar) || normalizeFileVariable(siblingBaseVar)
540
548
 
541
549
  // Create or get entry for this sibling variable
542
550
  if (!uniqueVariablesMap.has(normalizedSiblingVar)) {
@@ -665,7 +673,7 @@ async function enrichMetadata(
665
673
  }
666
674
 
667
675
  // Normalize file paths after variable substitution
668
- resolvedVariable = normalizeFileVariable(resolvedVariable)
676
+ resolvedVariable = normalizeDotenvFileVariable(resolvedVariable) || normalizeFileVariable(resolvedVariable)
669
677
 
670
678
  // Update the variable to the resolved version and update map key
671
679
  if (resolvedVariable !== baseVar) {
@@ -712,13 +720,22 @@ async function enrichMetadata(
712
720
 
713
721
  if (!hasVariables) {
714
722
  // Look up in fileRefsFound to see if file exists
715
- const fileRef = fileRefsFound.find(ref => ref.relativePath === filePath)
723
+ const fileRef = fileRefsFound.find(ref => sameFileRefPath(ref.relativePath, filePath))
716
724
  if (fileRef) {
717
725
  entry.fileExists = fileRef.exists
726
+ applyDotenvFileRefMetadata(entry, {
727
+ ...fileRef,
728
+ variable: entry.variable,
729
+ })
718
730
  } else if (configPath) {
719
731
  const thePath = path.resolve(path.dirname(configPath), filePath)
720
732
  const fileExists = fs.existsSync(thePath)
721
733
  entry.fileExists = fileExists
734
+ applyDotenvFileRefMetadata(entry, {
735
+ filePath: thePath,
736
+ relativePath: filePath,
737
+ variable: entry.variable,
738
+ })
722
739
  }
723
740
  }
724
741
  }
@@ -0,0 +1,134 @@
1
+ const path = require('path')
2
+ const { normalizePath } = require('../paths/filePathUtils')
3
+ const { splitCsv } = require('../strings/splitCsv')
4
+ const { trimSurroundingQuotes } = require('../strings/quoteUtils')
5
+
6
+ const DOTENV_SENSITIVITY_REASON = 'dotenv_file'
7
+
8
+ function isDotenvFilePath(filePath) {
9
+ if (!filePath || typeof filePath !== 'string') return false
10
+ const cleanPath = filePath.trim().replace(/^["']|["']$/g, '')
11
+ const baseName = path.basename(cleanPath)
12
+ return baseName === '.env' || baseName.startsWith('.env.')
13
+ }
14
+
15
+ function hasFileAccessor(variableString) {
16
+ const expression = extractFileExpression(variableString)
17
+ if (!expression) return false
18
+ const suffix = String(variableString || '').slice(expression.end).trim()
19
+ return suffix.startsWith(':') || suffix.startsWith('.')
20
+ }
21
+
22
+ function extractFileExpression(variableString) {
23
+ const value = String(variableString || '')
24
+ const prefixMatch = /(?:^|[^\w.])((?:file|text)\()/.exec(value)
25
+ if (!prefixMatch) return null
26
+
27
+ const expressionStart = prefixMatch.index + prefixMatch[0].indexOf(prefixMatch[1])
28
+ const openParenIndex = expressionStart + prefixMatch[1].length - 1
29
+ let depth = 1
30
+ let index = openParenIndex + 1
31
+
32
+ while (index < value.length && depth > 0) {
33
+ if (value[index] === '(') depth++
34
+ else if (value[index] === ')') depth--
35
+ index++
36
+ }
37
+
38
+ if (depth !== 0) return null
39
+
40
+ const fileContent = value.substring(openParenIndex + 1, index - 1).trim()
41
+ if (!fileContent) return null
42
+
43
+ const parts = splitCsv(fileContent, undefined, { protectVariables: true })
44
+ const filePath = trimSurroundingQuotes(parts[0].trim(), false)
45
+
46
+ return {
47
+ filePath,
48
+ expression: value.slice(expressionStart, index),
49
+ start: expressionStart,
50
+ end: index,
51
+ }
52
+ }
53
+
54
+ function extractDotenvPath(source) {
55
+ const candidates = [
56
+ source.relativePath,
57
+ source.resolvedPath,
58
+ source.filePath,
59
+ source.fullFilePath,
60
+ ].filter(Boolean)
61
+
62
+ for (const candidate of candidates) {
63
+ if (isDotenvFilePath(candidate)) return candidate
64
+ }
65
+
66
+ const variableCandidates = [
67
+ source.variable,
68
+ source.variableString,
69
+ source.originalVariableString,
70
+ source.resolvedVariableString,
71
+ ].filter(Boolean)
72
+
73
+ for (const candidate of variableCandidates) {
74
+ const extracted = extractFileExpression(String(candidate))
75
+ if (extracted && isDotenvFilePath(extracted.filePath)) {
76
+ return extracted.filePath
77
+ }
78
+ }
79
+
80
+ return null
81
+ }
82
+
83
+ function getDotenvFileRefMetadata(source = {}) {
84
+ const dotenvPath = extractDotenvPath(source)
85
+ if (!dotenvPath) return null
86
+
87
+ const scopeSource = source.variableString ||
88
+ source.variable ||
89
+ source.originalVariableString ||
90
+ source.resolvedVariableString ||
91
+ ''
92
+ const accessorScope = hasFileAccessor(scopeSource) ? 'key' : null
93
+
94
+ return {
95
+ sensitive: true,
96
+ sensitivityReason: DOTENV_SENSITIVITY_REASON,
97
+ dotenvFile: true,
98
+ dotenvReadScope: accessorScope || source.dotenvReadScope || 'full_file',
99
+ }
100
+ }
101
+
102
+ function normalizeDotenvFileVariable(variableString) {
103
+ const value = String(variableString || '')
104
+ const expression = extractFileExpression(value)
105
+ if (!expression || !isDotenvFilePath(expression.filePath)) return null
106
+
107
+ const normalizedPath = normalizePath(expression.filePath) || expression.filePath
108
+ const normalizedExpression = expression.expression.replace(/\(([\s\S]*)\)$/, `(${normalizedPath})`)
109
+ const suffix = value.slice(expression.end).trim()
110
+ const accessor = suffix.match(/^([:.][\w.[\]-]+)/)
111
+
112
+ return accessor ? `${normalizedExpression}${accessor[1]}` : normalizedExpression
113
+ }
114
+
115
+ function isIniLikeFilePath(filePath) {
116
+ const ext = path.extname(String(filePath || '')).slice(1).toLowerCase()
117
+ return ext === 'ini' || isDotenvFilePath(filePath)
118
+ }
119
+
120
+ function applyDotenvFileRefMetadata(target, source) {
121
+ const metadata = getDotenvFileRefMetadata(source)
122
+ if (metadata) Object.assign(target, metadata)
123
+ return target
124
+ }
125
+
126
+ module.exports = {
127
+ DOTENV_SENSITIVITY_REASON,
128
+ applyDotenvFileRefMetadata,
129
+ getDotenvFileRefMetadata,
130
+ hasFileAccessor,
131
+ normalizeDotenvFileVariable,
132
+ isDotenvFilePath,
133
+ isIniLikeFilePath,
134
+ }
@@ -0,0 +1,47 @@
1
+ const { test } = require('uvu')
2
+ const assert = require('uvu/assert')
3
+ const {
4
+ getDotenvFileRefMetadata,
5
+ hasFileAccessor,
6
+ isDotenvFilePath,
7
+ normalizeDotenvFileVariable,
8
+ } = require('./dotenvFileRefs')
9
+
10
+ test('dotenv file detection matches .env and .env variants', () => {
11
+ assert.is(isDotenvFilePath('.env'), true)
12
+ assert.is(isDotenvFilePath('./.env.production'), true)
13
+ assert.is(isDotenvFilePath('/tmp/app/.env.local'), true)
14
+ assert.is(isDotenvFilePath('./env.yml'), false)
15
+ })
16
+
17
+ test('dotenv accessors are detected without assuming ${} syntax', () => {
18
+ assert.is(hasFileAccessor('file(.env).API_KEY'), true)
19
+ assert.is(hasFileAccessor('file(.env):TOKEN'), true)
20
+ assert.is(hasFileAccessor('${file(.env).API_KEY}'), true)
21
+ assert.is(hasFileAccessor('[[file(.env).API_KEY]]'), true)
22
+ assert.is(hasFileAccessor('file(.env)'), false)
23
+ })
24
+
25
+ test('dotenv metadata classifies full-file and key reads', () => {
26
+ assert.equal(getDotenvFileRefMetadata({ variable: 'file(.env)' }), {
27
+ sensitive: true,
28
+ sensitivityReason: 'dotenv_file',
29
+ dotenvFile: true,
30
+ dotenvReadScope: 'full_file',
31
+ })
32
+ assert.equal(getDotenvFileRefMetadata({ variable: '[[file(.env):TOKEN]]' }), {
33
+ sensitive: true,
34
+ sensitivityReason: 'dotenv_file',
35
+ dotenvFile: true,
36
+ dotenvReadScope: 'key',
37
+ })
38
+ })
39
+
40
+ test('dotenv variable normalization preserves key accessors', () => {
41
+ assert.is(normalizeDotenvFileVariable('file(.env)'), 'file(./.env)')
42
+ assert.is(normalizeDotenvFileVariable('file(.env).API_KEY'), 'file(./.env).API_KEY')
43
+ assert.is(normalizeDotenvFileVariable('file(.env):TOKEN'), 'file(./.env):TOKEN')
44
+ assert.is(normalizeDotenvFileVariable('file(config.yml):value'), null)
45
+ })
46
+
47
+ test.run()