configorama 0.11.0 → 1.0.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.
Files changed (78) hide show
  1. package/README.md +429 -123
  2. package/cli.js +282 -49
  3. package/index.d.ts +43 -1
  4. package/package.json +5 -1
  5. package/src/capabilities.js +59 -0
  6. package/src/capabilities.test.js +44 -0
  7. package/src/display.js +70 -7
  8. package/src/display.test.js +82 -0
  9. package/src/errors.js +73 -0
  10. package/src/index.js +91 -1
  11. package/src/main.js +159 -19
  12. package/src/parsers/esm.js +1 -16
  13. package/src/parsers/typescript.js +1 -48
  14. package/src/resolvers/valueFromCron.js +4 -25
  15. package/src/resolvers/valueFromEval.js +11 -1
  16. package/src/resolvers/valueFromFile.js +8 -1
  17. package/src/resolvers/valueFromGit.js +43 -17
  18. package/src/resolvers/valueFromOptions.js +5 -4
  19. package/src/utils/filters/filterArgs.js +57 -0
  20. package/src/utils/filters/oneOf.js +77 -0
  21. package/src/utils/introspection/audit.js +78 -0
  22. package/src/utils/introspection/graph.js +43 -0
  23. package/src/utils/introspection/model.js +150 -0
  24. package/src/utils/introspection/model.test.js +93 -0
  25. package/src/utils/parsing/commentAnnotations.js +107 -0
  26. package/src/utils/parsing/commentAnnotations.test.js +123 -0
  27. package/src/utils/parsing/enrichMetadata.js +64 -1
  28. package/src/utils/parsing/enrichMetadata.test.js +84 -0
  29. package/src/utils/parsing/extractComment.js +145 -0
  30. package/src/utils/parsing/extractComment.test.js +182 -0
  31. package/src/utils/parsing/preProcess.js +2 -1
  32. package/src/utils/paths/findLineForKey.js +2 -2
  33. package/src/utils/paths/ignorePaths.js +22 -9
  34. package/src/utils/redaction/redact.js +78 -0
  35. package/src/utils/redaction/redact.test.js +38 -0
  36. package/src/utils/redaction/setupRedaction.js +47 -0
  37. package/src/utils/redaction/setupRedaction.test.js +68 -0
  38. package/src/utils/requirements/configRequirements.js +351 -0
  39. package/src/utils/requirements/configRequirements.test.js +380 -0
  40. package/src/utils/requirements/serializeRequirements.js +120 -0
  41. package/src/utils/requirements/serializeRequirements.test.js +211 -0
  42. package/src/utils/security/evalSafety.js +86 -0
  43. package/src/utils/security/evalSafety.test.js +61 -0
  44. package/src/utils/security/safetyPolicy.js +110 -0
  45. package/src/utils/security/safetyPolicy.test.js +29 -0
  46. package/src/utils/strings/didYouMean.js +70 -0
  47. package/src/utils/strings/didYouMean.test.js +52 -0
  48. package/src/utils/strings/formatFunctionArgs.js +6 -1
  49. package/src/utils/strings/splitByComma.js +5 -0
  50. package/src/utils/ui/configWizard.js +208 -34
  51. package/src/utils/ui/createEditorLink.js +17 -1
  52. package/src/utils/ui/promptDescriptors.js +196 -0
  53. package/src/utils/ui/promptDescriptors.test.js +162 -0
  54. package/src/utils/variables/cleanVariable.js +22 -0
  55. package/src/utils/variables/getVariableType.js +1 -0
  56. package/types/src/index.d.ts +0 -24
  57. package/types/src/index.d.ts.map +1 -1
  58. package/types/src/main.d.ts +16 -8
  59. package/types/src/main.d.ts.map +1 -1
  60. package/types/src/resolvers/valueFromFile.d.ts +0 -2
  61. package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
  62. package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
  63. package/types/src/resolvers/valueFromSelf.d.ts +1 -0
  64. package/types/src/resolvers/valueFromSelf.d.ts.map +1 -0
  65. package/types/src/utils/parsing/parse.d.ts.map +1 -1
  66. package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
  67. package/types/src/utils/paths/findLineForKey.d.ts +0 -9
  68. package/types/src/utils/paths/findLineForKey.d.ts.map +1 -1
  69. package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
  70. package/types/src/utils/variables/variableUtils.d.ts +1 -1
  71. package/types/src/display.d.ts +0 -62
  72. package/types/src/display.d.ts.map +0 -1
  73. package/types/src/metadata.d.ts +0 -28
  74. package/types/src/metadata.d.ts.map +0 -1
  75. package/types/src/utils/BoundedMap.d.ts +0 -10
  76. package/types/src/utils/BoundedMap.d.ts.map +0 -1
  77. package/types/src/utils/paths/ignorePaths.d.ts +0 -5
  78. package/types/src/utils/paths/ignorePaths.d.ts.map +0 -1
package/src/main.js CHANGED
@@ -39,7 +39,25 @@ function walkAndUpdate(root, callback) {
39
39
  }
40
40
  visit(root, [], null, null)
41
41
  }
42
+
43
+ function isNestedFilterArgument(property, matchedString) {
44
+ if (typeof property !== 'string' || typeof matchedString !== 'string') return false
45
+ if (property.trim() === matchedString.trim()) return false
46
+ const matchIdx = property.indexOf(matchedString)
47
+ const pipeIdx = property.indexOf('|')
48
+ const openParenIdx = property.lastIndexOf('(', matchIdx)
49
+ const closeParenIdx = property.indexOf(')', matchIdx)
50
+ return pipeIdx !== -1 && matchIdx > pipeIdx && openParenIdx > pipeIdx && closeParenIdx > matchIdx
51
+ }
42
52
  const dotProp = require('dot-prop')
53
+
54
+ function resolveStaticFilterArg(arg, config) {
55
+ const match = String(arg).trim().match(/^\$\{(?:self:)?([^}]+)\}$/)
56
+ if (!match) return arg
57
+ if (!dotProp.has(config, match[1])) return arg
58
+ return encodeFilterArg(dotProp.get(config, match[1]))
59
+ }
60
+
43
61
  /* Utils - root */
44
62
  const {
45
63
  isArray, isString, isNumber, isObject, isDate, isRegExp, isFunction,
@@ -71,17 +89,28 @@ const { replaceAll } = require('./utils/strings/replaceAll')
71
89
  const { getTextAfterOccurrence, findNestedVariable } = require('./utils/strings/textUtils')
72
90
  const { ensureQuote, isSurroundedByQuotes, startsWithQuotedPipe } = require('./utils/strings/quoteUtils')
73
91
  const { splitOnPipe } = require('./utils/strings/splitOnPipe')
92
+ const { encodeFilterArg } = require('./utils/filters/filterArgs')
93
+ const { validateOneOf } = require('./utils/filters/oneOf')
74
94
  /* Utils - ui */
75
95
  const chalk = require('./utils/ui/chalk')
76
96
  const deepLog = require('./utils/ui/deep-log')
77
97
  const { logHeader } = require('./utils/ui/logs')
78
98
  const { runConfigWizard } = require('./utils/ui/configWizard')
99
+ const { buildConfigRequirements } = require('./utils/requirements/configRequirements')
100
+ const { redactUserInputsByRequirements } = require('./utils/redaction/setupRedaction')
79
101
  /* Display */
80
102
  const { displayNoVariablesFound, displayVariableDetails, displayUniqueVariables, displayConfigurableVariables } = require('./display')
81
103
  /* Metadata */
82
104
  const { collectVariableMetadata: collectMetadata } = require('./metadata')
83
105
  /* Utils - validation */
84
106
  const { warnIfNotFound, isValidValue } = require('./utils/validation/warnIfNotFound')
107
+ const {
108
+ assertCustomFunctionsAllowed,
109
+ assertCustomResolversAllowed,
110
+ assertSafeConfigInput,
111
+ normalizeSafetyPolicy,
112
+ } = require('./utils/security/safetyPolicy')
113
+ const { ConfigoramaError } = require('./errors')
85
114
  /* Utils - variables */
86
115
  const cleanVariable = require('./utils/variables/cleanVariable')
87
116
  const appendDeepVariable = require('./utils/variables/appendDeepVariable')
@@ -144,6 +173,8 @@ class Configorama {
144
173
  }
145
174
 
146
175
  const options = opts || {}
176
+ // Setup wizard runs when --setup flag is passed (CLI) or options.setup is true (library)
177
+ this.setupMode = SETUP_MODE || options.setup === true
147
178
  // Set opts to pass into JS file calls
148
179
  this.settings = Object.assign({}, {
149
180
  // Allow unknown ${xyz:...} syntax where xyz is not a registered resolver
@@ -168,6 +199,7 @@ class Configorama {
168
199
  // CloudFormation Fn::Sub, inline Lambda code, and CloudFront functions.
169
200
  ignorePaths: [],
170
201
  skipResolutionPaths: [],
202
+ safeMode: false,
171
203
  }, options)
172
204
 
173
205
  // Backward compat: allowUnknownVars -> allowUnknownVariableTypes
@@ -179,6 +211,13 @@ class Configorama {
179
211
  this.settings.allowUnknownVariableTypes = options.allowUnknownVariables
180
212
  }
181
213
 
214
+ this.safetyPolicy = normalizeSafetyPolicy(this.settings, {
215
+ configDir: options.configDir || (typeof fileOrObject === 'string' ? path.dirname(path.resolve(fileOrObject)) : process.cwd())
216
+ })
217
+
218
+ assertCustomResolversAllowed(options.variableSources, this.safetyPolicy)
219
+ assertCustomFunctionsAllowed(options.functions, this.safetyPolicy)
220
+
182
221
  // Merge legacy allowUnknownParams and allowUnknownFileRefs into allowUnresolvedVariables
183
222
  let unresolvedSetting = this.settings.allowUnresolvedVariables
184
223
  if (unresolvedSetting !== true) {
@@ -197,6 +236,9 @@ class Configorama {
197
236
  // Paths whose current value is a literal with no variables — skip rebuilding
198
237
  // their leaf object on every subsequent populateObjectImpl iteration.
199
238
  this._resolvedPaths = new Set()
239
+ // Ignore-path decisions are constant for a run (patterns are fixed per
240
+ // instance), so cache per path to skip repeated glob matching.
241
+ this._ignorePathCache = new Map()
200
242
  // Cache raw file contents per absolute path so repeated ${file:...} refs
201
243
  // to the same file (e.g., merged twice into different keys) don't reread.
202
244
  this._fileContentCache = new Map()
@@ -208,7 +250,7 @@ class Configorama {
208
250
  this._needsRawClone = !!(
209
251
  this.settings.returnMetadata ||
210
252
  this.settings.returnPreResolvedVariableDetails ||
211
- VERBOSE || SETUP_MODE || showFound
253
+ VERBOSE || this.setupMode || showFound
212
254
  )
213
255
 
214
256
  this.foundVariables = []
@@ -272,6 +314,7 @@ class Configorama {
272
314
  // Set configPath for file references
273
315
  this.configPath = options.configDir || process.cwd()
274
316
  } else if (typeof fileOrObject === 'string') {
317
+ assertSafeConfigInput(fileOrObject, this.safetyPolicy)
275
318
  // read and parse file
276
319
  const fileContents = fs.readFileSync(fileOrObject, 'utf-8')
277
320
  const fileDirectory = path.dirname(path.resolve(fileOrObject))
@@ -372,7 +415,12 @@ class Configorama {
372
415
  description: `Resolves values from files. Supports sub-properties via :key or .key lookup.`,
373
416
  match: fileRefSyntax,
374
417
  resolver: (varString, o, x, pathValue) => {
375
- return this.getValueFromFile(varString, { context: pathValue })
418
+ // Inside ignore-path contexts (e.g. Fn::Sub) inline the file as raw text so
419
+ // embedded CloudFormation refs survive and the body stays a string. Skip
420
+ // raw mode when a :key/.key accessor is present — that needs a parsed value.
421
+ const hasAccessor = /\)\s*[:.]/.test(varString)
422
+ const asRawText = !!(pathValue && this.isIgnorePath(pathValue.path)) && !hasAccessor
423
+ return this.getValueFromFile(varString, { asRawText, context: pathValue })
376
424
  },
377
425
  },
378
426
 
@@ -484,12 +532,24 @@ class Configorama {
484
532
  )
485
533
  this.variablesKnownTypes = variablesKnownTypes
486
534
 
535
+ // Explicit configorama types that should still resolve inside ignore-path
536
+ // contexts like Fn::Sub (file, text, env, opt, cron, git, user sources, ...).
537
+ // Excludes self/dot.prop refs — those are left verbatim for CloudFormation /
538
+ // downstream Serverless resolution.
539
+ this.subResolvableTypes = combineRegexes(
540
+ /** @type {RegExp[]} */ (this.variableTypes
541
+ .filter((v) => v.type !== 'string' && v.type !== 'self' && v.type !== 'dot.prop' && v.match instanceof RegExp)
542
+ .map((v) => v.match))
543
+ )
544
+
487
545
  // Build prefix lookup map for O(1) type detection (perf optimization)
488
546
  this._resolverByPrefix = new Map()
489
547
  for (const r of this.variableTypes) {
490
- const prefix = r.prefix || r.type
491
- if (prefix && r.match instanceof RegExp && !r.internal) {
492
- this._resolverByPrefix.set(prefix + ':', r)
548
+ const prefixes = r.prefixes || [r.prefix || r.type]
549
+ for (const prefix of prefixes) {
550
+ if (prefix && r.match instanceof RegExp && !r.internal) {
551
+ this._resolverByPrefix.set(prefix + ':', r)
552
+ }
493
553
  }
494
554
  }
495
555
 
@@ -560,6 +620,37 @@ class Configorama {
560
620
  if (value === undefined || value === null || value === 'null') return ''
561
621
  return String(value)
562
622
  },
623
+ Array: (value) => {
624
+ if (Array.isArray(value)) return value
625
+ if (typeof value !== 'string') {
626
+ throw new Error(`Configorama Error: Expected Array, got "${value}"`)
627
+ }
628
+ const trimmed = value.trim()
629
+ if (!trimmed) return []
630
+ try {
631
+ const parsed = JSON5.parse(trimmed)
632
+ if (Array.isArray(parsed)) return parsed
633
+ throw new Error('not-array')
634
+ } catch (error) {
635
+ if (trimmed.includes(',')) {
636
+ return trimmed.split(',').map(item => item.trim()).filter(Boolean)
637
+ }
638
+ throw new Error(`Configorama Error: Expected Array, got "${value}"`)
639
+ }
640
+ },
641
+ Object: (value) => {
642
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value
643
+ if (typeof value !== 'string') {
644
+ throw new Error(`Configorama Error: Expected Object, got "${value}"`)
645
+ }
646
+ try {
647
+ const parsed = JSON5.parse(value)
648
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed
649
+ } catch (error) {
650
+ // Fall through to consistent error below.
651
+ }
652
+ throw new Error(`Configorama Error: Expected Object, got "${value}"`)
653
+ },
563
654
  Json: (value) => {
564
655
  try {
565
656
  return typeof value === 'string' ? JSON.parse(value) : value
@@ -573,6 +664,7 @@ class Configorama {
573
664
  // The helpText argument is extracted during metadata collection for the wizard
574
665
  return value
575
666
  },
667
+ oneOf: validateOneOf,
576
668
  }
577
669
 
578
670
  // Apply user defined filters
@@ -731,7 +823,7 @@ class Configorama {
731
823
  dynamicArgs: this.settings.dynamicArgs
732
824
  })
733
825
  this.configFileContents = ''
734
- if (VERBOSE || showFoundVariables || this.settings.returnPreResolvedVariableDetails || SETUP_MODE) {
826
+ if (VERBOSE || showFoundVariables || this.settings.returnPreResolvedVariableDetails || this.setupMode) {
735
827
  this.configFileContents = fs.readFileSync(this.configFilePath, 'utf8')
736
828
  }
737
829
  /*
@@ -774,7 +866,7 @@ class Configorama {
774
866
  const variableSyntax = this.variableSyntax
775
867
  const variablesKnownTypes = this.variablesKnownTypes
776
868
 
777
- if (VERBOSE || showFoundVariables || this.settings.returnPreResolvedVariableDetails || SETUP_MODE) {
869
+ if (VERBOSE || showFoundVariables || this.settings.returnPreResolvedVariableDetails || this.setupMode) {
778
870
  const metadata = this.collectVariableMetadata()
779
871
 
780
872
  const enrich = await enrichMetadata(
@@ -829,15 +921,18 @@ class Configorama {
829
921
  displayConfigurableVariables(displayParams)
830
922
 
831
923
 
832
- // WALK through CLI prompt if --setup flag is set
833
- if (SETUP_MODE) {
924
+ // WALK through CLI prompt when setup mode is active
925
+ if (this.setupMode) {
834
926
  logHeader('Setup Mode')
835
927
  // deepLog('enrich', enrich)
836
928
  const userInputs = await runConfigWizard(enrich, this.originalConfig, this.configFilePath)
929
+ const setupRequirements = buildConfigRequirements(enrich)
930
+ this.setupRequirements = setupRequirements
931
+ const displayInputs = redactUserInputsByRequirements(userInputs, setupRequirements)
837
932
 
838
933
  logHeader('User Inputs Summary')
839
934
  console.log()
840
- console.log(JSON.stringify(userInputs, null, 2))
935
+ console.log(JSON.stringify(displayInputs, null, 2))
841
936
 
842
937
  // TODO set values
843
938
 
@@ -887,6 +982,12 @@ class Configorama {
887
982
 
888
983
  const useDotEnv = this.originalConfig.useDotenv || this.originalConfig.useDotEnv
889
984
  if ((useDotEnv && useDotEnv === true) || this.settings.useDotEnvFiles) {
985
+ if (this.safetyPolicy.blockDotEnv) {
986
+ throw new ConfigoramaError('blocked_by_safe_mode', 'Dotenv loading is blocked in safe mode', {
987
+ surface: 'dotenv',
988
+ configPath: this.configFilePath,
989
+ })
990
+ }
890
991
  let providerStage
891
992
  /* has hardcoded stage */
892
993
  if (
@@ -1111,8 +1212,29 @@ class Configorama {
1111
1212
  // #######################
1112
1213
  // ## PROPERTY HANDLING ##
1113
1214
  // #######################
1114
- shouldSkipResolution(pathValue) {
1115
- return shouldIgnorePath(pathValue, this.ignorePathPatterns)
1215
+ isIgnorePath(pathValue) {
1216
+ if (!this.ignorePathPatterns.length) return false
1217
+ // NUL-join so distinct path arrays can never collide on a shared key.
1218
+ const key = isArray(pathValue) ? pathValue.join('\x00') : String(pathValue)
1219
+ const cached = this._ignorePathCache.get(key)
1220
+ if (cached !== undefined) return cached
1221
+ const result = shouldIgnorePath(pathValue, this.ignorePathPatterns)
1222
+ this._ignorePathCache.set(key, result)
1223
+ return result
1224
+ }
1225
+ // True when the value has a configorama-typed token that resolves even inside an
1226
+ // ignore-path (file/text/env/opt/cron/git/custom) — i.e. not just self/CFN refs.
1227
+ hasSubResolvableToken(value) {
1228
+ if (typeof value !== 'string') return false
1229
+ const matches = this.getMatches(value)
1230
+ if (!isArray(matches)) return false
1231
+ return matches.some((m) => this.subResolvableTypes.test(m.variable))
1232
+ }
1233
+ shouldSkipResolution(pathValue, value) {
1234
+ if (!this.isIgnorePath(pathValue)) return false
1235
+ // Under an ignore path (Fn::Sub etc.) keep resolving configorama's own typed
1236
+ // refs; only skip when nothing but self/CFN refs remain.
1237
+ return !this.hasSubResolvableToken(value)
1116
1238
  }
1117
1239
 
1118
1240
  /**
@@ -1275,7 +1397,7 @@ class Configorama {
1275
1397
  /* Leave opaque paths verbatim. These often contain non-configorama
1276
1398
  `${...}` syntax from CloudFormation, JavaScript, shell, VTL, etc. */
1277
1399
  variables = variables.filter((property) => {
1278
- return !this.shouldSkipResolution(property.path)
1400
+ return !this.shouldSkipResolution(property.path, property.value)
1279
1401
  })
1280
1402
  /*
1281
1403
  console.log(`variables at call count ${this.callCount}`, variables)
@@ -1563,7 +1685,7 @@ class Configorama {
1563
1685
  console.log(valueObject)
1564
1686
  }
1565
1687
  const property = valueObject.value
1566
- if (this.shouldSkipResolution(valueObject.path)) {
1688
+ if (this.shouldSkipResolution(valueObject.path, property)) {
1567
1689
  return Promise.resolve(property)
1568
1690
  }
1569
1691
  const matches = this.getMatches(property)
@@ -1779,6 +1901,9 @@ class Configorama {
1779
1901
  valueToPopulate = `"${valueToPopulate}"`
1780
1902
  }
1781
1903
  }
1904
+ if (isNestedFilterArgument(property, currentMatchedString)) {
1905
+ valueToPopulate = encodeFilterArg(valueToPopulate)
1906
+ }
1782
1907
  property = replaceAll(currentMatchedString, valueToPopulate, property)
1783
1908
  // console.log('property replaceAll', property)
1784
1909
 
@@ -1789,7 +1914,10 @@ class Configorama {
1789
1914
  // partial replacement, number
1790
1915
  } else if (isNumber(valueToPopulate)) {
1791
1916
  if (DEBUG_TYPE) console.log('DEBUG_TYPE isNumber')
1792
- property = replaceAll(matchedString, String(valueToPopulate), property)
1917
+ const replacementValue = isNestedFilterArgument(property, matchedString)
1918
+ ? encodeFilterArg(valueToPopulate)
1919
+ : String(valueToPopulate)
1920
+ property = replaceAll(matchedString, replacementValue, property)
1793
1921
  // TODO This was temp fix for array value mismatch from filters. This fixes filterInner: ${commas | split(${self:inner}, 2) }
1794
1922
  // } else if (isArray(valueToPopulate) && valueToPopulate.length === 1) {
1795
1923
  // property = replaceAll(matchedString, String(valueToPopulate[0]), property)
@@ -1812,7 +1940,9 @@ class Configorama {
1812
1940
  )
1813
1941
  // Only encode for file() or text() references where JSON braces break regex matching
1814
1942
  const isFileOrTextRef = /\bfile\s*\(|\btext\s*\(/.test(property)
1815
- if (isNestedInVariable && isFileOrTextRef) {
1943
+ if (isNestedFilterArgument(property, matchedString)) {
1944
+ property = replaceAll(matchedString, encodeFilterArg(valueToPopulate), property)
1945
+ } else if (isNestedInVariable && isFileOrTextRef) {
1816
1946
  // Encode object as base64 to avoid breaking variable syntax with nested braces
1817
1947
  const encodedObj = encodeJsonForVariable(valueToPopulate)
1818
1948
  property = replaceAll(matchedString, encodedObj, property)
@@ -2040,7 +2170,7 @@ Missing Value ${missingValue} - ${matchedString}
2040
2170
  const rawArgs = funcMatch[2]
2041
2171
  if (rawArgs) {
2042
2172
  const splitter = splitCsv(rawArgs, ', ')
2043
- filterArgs = formatFunctionArgs(splitter)
2173
+ filterArgs = formatFunctionArgs(splitter.map(arg => resolveStaticFilterArg(arg, this.config)))
2044
2174
  }
2045
2175
  }
2046
2176
 
@@ -2166,6 +2296,7 @@ Missing Value ${missingValue} - ${matchedString}
2166
2296
  // Cache joined path to avoid repeated array.join('.') calls
2167
2297
  const pathJoined = pathValue && pathValue.length ? pathValue.join('.') : null
2168
2298
 
2299
+
2169
2300
  // Track every call to getValueFromSource for metadata
2170
2301
  if (this._trackCalls && pathJoined) {
2171
2302
  const pathKey = pathJoined
@@ -2318,6 +2449,14 @@ Missing Value ${missingValue} - ${matchedString}
2318
2449
  // console.log('resolverFunction', resolverFunction)
2319
2450
  /** */
2320
2451
 
2452
+ // Inside ignore-path contexts (Fn::Sub, inline code, VTL templates, ...) leave
2453
+ // self refs, bare config refs, and CloudFormation refs verbatim for CloudFormation
2454
+ // / downstream Serverless to resolve. Everything configorama can resolve on its own
2455
+ // (file/text/env/opt/cron/eval/git/custom/string/number) still resolves.
2456
+ if (this.isIgnorePath(pathValue) && (!found || resolverType === 'self' || resolverType === 'dot.prop')) {
2457
+ return Promise.resolve(encodeUnknown(this.varPrefix + variableString + this.varSuffix))
2458
+ }
2459
+
2321
2460
  if (found && resolverFunction) {
2322
2461
  /*
2323
2462
  console.log(`----------Resolver [${resolverType}]----------------------`)
@@ -2477,7 +2616,7 @@ Missing Value ${missingValue} - ${matchedString}
2477
2616
  // Parse arguments using the same logic as functions
2478
2617
  if (rawArgs) {
2479
2618
  const splitter = splitCsv(rawArgs, ', ')
2480
- filterArgs = formatFunctionArgs(splitter)
2619
+ filterArgs = formatFunctionArgs(splitter.map(arg => resolveStaticFilterArg(arg, this.config)))
2481
2620
  }
2482
2621
  }
2483
2622
 
@@ -2749,7 +2888,8 @@ Missing Value ${missingValue} - ${matchedString}
2749
2888
  textRefSyntax: textRefSyntax,
2750
2889
  varPrefix: this.varPrefix,
2751
2890
  varSuffix: this.varSuffix,
2752
- fileContentCache: this._fileContentCache
2891
+ fileContentCache: this._fileContentCache,
2892
+ safetyPolicy: this.safetyPolicy
2753
2893
  }
2754
2894
  return getValueFromFileResolver(ctx, variableString, options)
2755
2895
  }
@@ -7,22 +7,7 @@ const path = require('path')
7
7
  * @returns {Promise<*>} The result of executing the ESM file
8
8
  */
9
9
  async function executeESMFile(filePath, opts = {}) {
10
- try {
11
- // Use require for now since ESM dynamic import in async context is complex
12
- // We'll use jiti to handle ESM syntax
13
- const { createJiti } = require('jiti')
14
- const jiti = createJiti(__filename, {
15
- interopDefault: true
16
- })
17
-
18
- // Load the ESM file - resolve to absolute path first
19
- const resolvedPath = path.resolve(filePath)
20
- let esmModule = jiti(resolvedPath)
21
-
22
- return esmModule
23
- } catch (err) {
24
- throw new Error(`Failed to load ESM file ${filePath}: ${err.message}`)
25
- }
10
+ return executeESMFileSync(filePath, opts)
26
11
  }
27
12
 
28
13
  /**
@@ -8,54 +8,7 @@ const fs = require('fs')
8
8
  * @returns {Promise<*>} The exported module from the TypeScript file
9
9
  */
10
10
  async function executeTypeScriptFile(filePath, opts = {}) {
11
- // Check if tsx is available first (preferred)
12
- let useTsx = false
13
- try {
14
- require.resolve('tsx/cjs/api')
15
- useTsx = true
16
- } catch (err) {
17
- // Fallback to ts-node if tsx is not available
18
- try {
19
- require.resolve('ts-node/register')
20
- } catch (tsNodeErr) {
21
- throw new Error(
22
- 'TypeScript support requires either "tsx" or "ts-node" to be installed. ' +
23
- 'Please install one of them:\n' +
24
- ' npm install tsx --save-dev (recommended)\n' +
25
- ' npm install ts-node typescript --save-dev'
26
- )
27
- }
28
- }
29
-
30
- // Clear require cache to ensure fresh execution
31
- const resolvedPath = require.resolve(filePath)
32
- delete require.cache[resolvedPath]
33
-
34
- let tsFile
35
- if (useTsx) {
36
- // Use tsx for modern, fast TypeScript execution
37
- // @ts-ignore - tsx doesn't have type declarations
38
- const { register } = require('tsx/cjs/api')
39
- const restore = register()
40
- try {
41
- tsFile = require(filePath)
42
- } catch (err) {
43
- throw new Error(`Failed to load TypeScript file: ${err.message}`)
44
- } finally {
45
- restore()
46
- }
47
- } else {
48
- // Fallback to ts-node
49
- try {
50
- // @ts-ignore - ts-node is optional peer dependency
51
- require('ts-node/register')
52
- tsFile = require(filePath)
53
- } catch (err) {
54
- throw new Error(`Failed to load TypeScript file with ts-node: ${err.message}`)
55
- }
56
- }
57
-
58
- return tsFile
11
+ return executeTypeScriptFileSync(filePath, opts)
59
12
  }
60
13
 
61
14
  /**
@@ -91,34 +91,13 @@ function parseCronExpression(input) {
91
91
  return `${minute} ${hour} * * *`
92
92
  }
93
93
 
94
- // Parse "every X minutes/hours/days" patterns
95
- const everyMatch = normalizedInput.match(/^every (\d+) (minute|minutes|hour|hours|day|days|week|weeks|month|months)s?$/i)
96
- if (everyMatch) {
97
- const interval = parseInt(everyMatch[1])
98
- const unit = everyMatch[2].toLowerCase().replace(/s$/, '') // Remove trailing 's' if present
99
-
100
- switch (unit) {
101
- case 'minute':
102
- return `*/${interval} * * * *`
103
- case 'hour':
104
- return `0 */${interval} * * *`
105
- case 'day':
106
- return `0 0 */${interval} * *`
107
- case 'week':
108
- return `0 0 * * 0/${interval}`
109
- case 'month':
110
- return `0 0 1 */${interval} *`
111
- default:
112
- throw new Error(`Unsupported interval unit: ${unit}`)
113
- }
114
- }
115
-
116
- // Parse "X minute(s)/hour(s)/day(s)" patterns (e.g., "1 minute", "5 minutes", "1 hour")
117
- const intervalMatch = normalizedInput.match(/^(\d+) (minute|minutes|hour|hours|day|days|week|weeks|month|months)s?$/i)
94
+ // Parse "every X minutes/hours/days" and bare "X minute(s)/hour(s)/day(s)" patterns
95
+ // (e.g., "every 5 minutes", "1 minute", "5 minutes", "1 hour")
96
+ const intervalMatch = normalizedInput.match(/^(?:every )?(\d+) (minute|minutes|hour|hours|day|days|week|weeks|month|months)s?$/i)
118
97
  if (intervalMatch) {
119
98
  const interval = parseInt(intervalMatch[1])
120
99
  const unit = intervalMatch[2].toLowerCase().replace(/s$/, '') // Remove trailing 's' if present
121
-
100
+
122
101
  switch (unit) {
123
102
  case 'minute':
124
103
  return `*/${interval} * * * *`
@@ -1,6 +1,7 @@
1
1
  // const evalRefSyntax = RegExp(/^eval\((~?[\{\}\:\${}a-zA=>+!-Z0-9._\-\/,'"\*\` ]+?)?\)/g)
2
2
  const evalRefSyntax = RegExp(/^eval\((.*)?\)/g)
3
3
  const { replaceOutsideQuotes } = require('../utils/strings/quoteAware')
4
+ const { assertSafeEvalExpression } = require('../utils/security/evalSafety')
4
5
 
5
6
  // Pattern for encoded objects/arrays: __OBJ:base64__ or __ARR:base64__
6
7
  const ENCODED_PATTERN = /__(?:OBJ|ARR):([A-Za-z0-9+/=]+)__/g
@@ -52,7 +53,9 @@ async function getValueFromEval(variableString) {
52
53
 
53
54
  // Use "justin" variant to support strict comparison (===, !==) and other JS-like operators
54
55
  try {
55
- const { default: subscript } = await import('subscript/justin')
56
+ // justin re-exports `parse` from subscript; importing the subpath directly
57
+ // ('subscript/parse') is not resolvable under classic module resolution.
58
+ const { default: subscript, parse } = await import('subscript/justin')
56
59
 
57
60
  // Handle string comparisons by ensuring both sides are quoted
58
61
  let processedExpression = expression.replace(/([a-zA-Z0-9_]+)\s*([=!<>]=?)\s*['"]([^'"]+)['"]/g, '"$1"$2"$3"')
@@ -79,6 +82,13 @@ async function getValueFromEval(variableString) {
79
82
  processedExpression = wrapComparisons(processedExpression)
80
83
 
81
84
  if (process.env.DEBUG_EVAL) console.log('eval processed:', processedExpression)
85
+
86
+ // Block prototype-chain escapes (e.g. "".constructor.constructor) before
87
+ // compiling, whether or not safe mode is enabled.
88
+ let ast = null
89
+ try { ast = parse(processedExpression) } catch (parseError) { ast = null }
90
+ assertSafeEvalExpression(processedExpression, ast)
91
+
82
92
  const fn = subscript(processedExpression)
83
93
  const result = fn(Object.keys(context).length > 0 ? context : undefined)
84
94
  return result
@@ -8,6 +8,7 @@ const { resolveFilePathFromMatch, resolveFilePath } = require('../utils/paths/ge
8
8
  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
+ const { checkFileAccess } = require('../utils/security/safetyPolicy')
11
12
 
12
13
  /* File Parsers */
13
14
  const YAML = require('../parsers/yaml')
@@ -109,13 +110,16 @@ function parseFileContents(content, filePath) {
109
110
  * @param {string} ctx.varPrefix - Variable prefix (e.g., '${')
110
111
  * @param {string} ctx.varSuffix - Variable suffix (e.g., '}')
111
112
  * @param {Map<string, string>} [ctx.fileContentCache] - Optional per-instance read cache keyed by absolute file path
113
+ * @param {object} [ctx.safetyPolicy] - Optional safe-mode policy for executable and root checks
112
114
  * @param {string} variableString - The variable string to resolve
113
115
  * @param {object} options - Resolution options
114
116
  * @returns {Promise<any>}
115
117
  */
116
118
  async function getValueFromFile(ctx, variableString, options) {
117
119
  const opts = options || {}
118
- const syntax = opts.asRawText ? ctx.textRefSyntax : ctx.fileRefSyntax
120
+ // Pick syntax from the ref keyword, not the raw-text flag, so a file() ref can
121
+ // also be inlined as raw text (e.g. inside an Fn::Sub) without losing its match.
122
+ const syntax = /^\s*text\(/.test(variableString) ? ctx.textRefSyntax : ctx.fileRefSyntax
119
123
  // console.log('From file', `"${variableString}"`)
120
124
  let matchedFileString = variableString.match(syntax)[0]
121
125
  // console.log('matchedFileString', matchedFileString)
@@ -185,6 +189,9 @@ async function getValueFromFile(ctx, variableString, options) {
185
189
  }
186
190
 
187
191
  const exists = fs.existsSync(fullFilePath)
192
+ if (ctx.safetyPolicy) {
193
+ checkFileAccess(fullFilePath, ctx.safetyPolicy, { variableString })
194
+ }
188
195
 
189
196
  const fileRefEntry = {
190
197
  filePath: fullFilePath,