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.
- package/README.md +429 -123
- package/cli.js +282 -49
- package/index.d.ts +43 -1
- package/package.json +5 -1
- package/src/capabilities.js +59 -0
- package/src/capabilities.test.js +44 -0
- package/src/display.js +70 -7
- package/src/display.test.js +82 -0
- package/src/errors.js +73 -0
- package/src/index.js +91 -1
- package/src/main.js +159 -19
- package/src/parsers/esm.js +1 -16
- package/src/parsers/typescript.js +1 -48
- package/src/resolvers/valueFromCron.js +4 -25
- package/src/resolvers/valueFromEval.js +11 -1
- package/src/resolvers/valueFromFile.js +8 -1
- package/src/resolvers/valueFromGit.js +43 -17
- package/src/resolvers/valueFromOptions.js +5 -4
- package/src/utils/filters/filterArgs.js +57 -0
- package/src/utils/filters/oneOf.js +77 -0
- package/src/utils/introspection/audit.js +78 -0
- package/src/utils/introspection/graph.js +43 -0
- package/src/utils/introspection/model.js +150 -0
- package/src/utils/introspection/model.test.js +93 -0
- package/src/utils/parsing/commentAnnotations.js +107 -0
- package/src/utils/parsing/commentAnnotations.test.js +123 -0
- package/src/utils/parsing/enrichMetadata.js +64 -1
- package/src/utils/parsing/enrichMetadata.test.js +84 -0
- package/src/utils/parsing/extractComment.js +145 -0
- package/src/utils/parsing/extractComment.test.js +182 -0
- package/src/utils/parsing/preProcess.js +2 -1
- package/src/utils/paths/findLineForKey.js +2 -2
- package/src/utils/paths/ignorePaths.js +22 -9
- package/src/utils/redaction/redact.js +78 -0
- package/src/utils/redaction/redact.test.js +38 -0
- package/src/utils/redaction/setupRedaction.js +47 -0
- package/src/utils/redaction/setupRedaction.test.js +68 -0
- package/src/utils/requirements/configRequirements.js +351 -0
- package/src/utils/requirements/configRequirements.test.js +380 -0
- package/src/utils/requirements/serializeRequirements.js +120 -0
- package/src/utils/requirements/serializeRequirements.test.js +211 -0
- package/src/utils/security/evalSafety.js +86 -0
- package/src/utils/security/evalSafety.test.js +61 -0
- package/src/utils/security/safetyPolicy.js +110 -0
- package/src/utils/security/safetyPolicy.test.js +29 -0
- package/src/utils/strings/didYouMean.js +70 -0
- package/src/utils/strings/didYouMean.test.js +52 -0
- package/src/utils/strings/formatFunctionArgs.js +6 -1
- package/src/utils/strings/splitByComma.js +5 -0
- package/src/utils/ui/configWizard.js +208 -34
- package/src/utils/ui/createEditorLink.js +17 -1
- package/src/utils/ui/promptDescriptors.js +196 -0
- package/src/utils/ui/promptDescriptors.test.js +162 -0
- package/src/utils/variables/cleanVariable.js +22 -0
- package/src/utils/variables/getVariableType.js +1 -0
- package/types/src/index.d.ts +0 -24
- package/types/src/index.d.ts.map +1 -1
- package/types/src/main.d.ts +16 -8
- package/types/src/main.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts +0 -2
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
- package/types/src/resolvers/valueFromSelf.d.ts +1 -0
- package/types/src/resolvers/valueFromSelf.d.ts.map +1 -0
- package/types/src/utils/parsing/parse.d.ts.map +1 -1
- package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
- package/types/src/utils/paths/findLineForKey.d.ts +0 -9
- package/types/src/utils/paths/findLineForKey.d.ts.map +1 -1
- package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
- package/types/src/utils/variables/variableUtils.d.ts +1 -1
- package/types/src/display.d.ts +0 -62
- package/types/src/display.d.ts.map +0 -1
- package/types/src/metadata.d.ts +0 -28
- package/types/src/metadata.d.ts.map +0 -1
- package/types/src/utils/BoundedMap.d.ts +0 -10
- package/types/src/utils/BoundedMap.d.ts.map +0 -1
- package/types/src/utils/paths/ignorePaths.d.ts +0 -5
- 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 ||
|
|
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
|
-
|
|
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
|
|
491
|
-
|
|
492
|
-
|
|
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 ||
|
|
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 ||
|
|
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
|
|
833
|
-
if (
|
|
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(
|
|
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
|
-
|
|
1115
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|
package/src/parsers/esm.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|