configorama 0.11.2 → 1.0.1
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 +132 -16
- package/src/resolvers/valueFromEval.js +11 -1
- package/src/resolvers/valueFromFile.js +12 -3
- 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 +21 -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/src/main.js
CHANGED
|
@@ -39,7 +39,39 @@ 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
|
+
}
|
|
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
|
+
|
|
42
66
|
const dotProp = require('dot-prop')
|
|
67
|
+
|
|
68
|
+
function resolveStaticFilterArg(arg, config) {
|
|
69
|
+
const match = String(arg).trim().match(/^\$\{(?:self:)?([^}]+)\}$/)
|
|
70
|
+
if (!match) return arg
|
|
71
|
+
if (!dotProp.has(config, match[1])) return arg
|
|
72
|
+
return encodeFilterArg(dotProp.get(config, match[1]))
|
|
73
|
+
}
|
|
74
|
+
|
|
43
75
|
/* Utils - root */
|
|
44
76
|
const {
|
|
45
77
|
isArray, isString, isNumber, isObject, isDate, isRegExp, isFunction,
|
|
@@ -71,17 +103,28 @@ const { replaceAll } = require('./utils/strings/replaceAll')
|
|
|
71
103
|
const { getTextAfterOccurrence, findNestedVariable } = require('./utils/strings/textUtils')
|
|
72
104
|
const { ensureQuote, isSurroundedByQuotes, startsWithQuotedPipe } = require('./utils/strings/quoteUtils')
|
|
73
105
|
const { splitOnPipe } = require('./utils/strings/splitOnPipe')
|
|
106
|
+
const { encodeFilterArg } = require('./utils/filters/filterArgs')
|
|
107
|
+
const { validateOneOf } = require('./utils/filters/oneOf')
|
|
74
108
|
/* Utils - ui */
|
|
75
109
|
const chalk = require('./utils/ui/chalk')
|
|
76
110
|
const deepLog = require('./utils/ui/deep-log')
|
|
77
111
|
const { logHeader } = require('./utils/ui/logs')
|
|
78
112
|
const { runConfigWizard } = require('./utils/ui/configWizard')
|
|
113
|
+
const { buildConfigRequirements } = require('./utils/requirements/configRequirements')
|
|
114
|
+
const { redactUserInputsByRequirements } = require('./utils/redaction/setupRedaction')
|
|
79
115
|
/* Display */
|
|
80
116
|
const { displayNoVariablesFound, displayVariableDetails, displayUniqueVariables, displayConfigurableVariables } = require('./display')
|
|
81
117
|
/* Metadata */
|
|
82
118
|
const { collectVariableMetadata: collectMetadata } = require('./metadata')
|
|
83
119
|
/* Utils - validation */
|
|
84
120
|
const { warnIfNotFound, isValidValue } = require('./utils/validation/warnIfNotFound')
|
|
121
|
+
const {
|
|
122
|
+
assertCustomFunctionsAllowed,
|
|
123
|
+
assertCustomResolversAllowed,
|
|
124
|
+
assertSafeConfigInput,
|
|
125
|
+
normalizeSafetyPolicy,
|
|
126
|
+
} = require('./utils/security/safetyPolicy')
|
|
127
|
+
const { ConfigoramaError } = require('./errors')
|
|
85
128
|
/* Utils - variables */
|
|
86
129
|
const cleanVariable = require('./utils/variables/cleanVariable')
|
|
87
130
|
const appendDeepVariable = require('./utils/variables/appendDeepVariable')
|
|
@@ -144,6 +187,8 @@ class Configorama {
|
|
|
144
187
|
}
|
|
145
188
|
|
|
146
189
|
const options = opts || {}
|
|
190
|
+
// Setup wizard runs when --setup flag is passed (CLI) or options.setup is true (library)
|
|
191
|
+
this.setupMode = SETUP_MODE || options.setup === true
|
|
147
192
|
// Set opts to pass into JS file calls
|
|
148
193
|
this.settings = Object.assign({}, {
|
|
149
194
|
// Allow unknown ${xyz:...} syntax where xyz is not a registered resolver
|
|
@@ -168,6 +213,7 @@ class Configorama {
|
|
|
168
213
|
// CloudFormation Fn::Sub, inline Lambda code, and CloudFront functions.
|
|
169
214
|
ignorePaths: [],
|
|
170
215
|
skipResolutionPaths: [],
|
|
216
|
+
safeMode: false,
|
|
171
217
|
}, options)
|
|
172
218
|
|
|
173
219
|
// Backward compat: allowUnknownVars -> allowUnknownVariableTypes
|
|
@@ -179,6 +225,13 @@ class Configorama {
|
|
|
179
225
|
this.settings.allowUnknownVariableTypes = options.allowUnknownVariables
|
|
180
226
|
}
|
|
181
227
|
|
|
228
|
+
this.safetyPolicy = normalizeSafetyPolicy(this.settings, {
|
|
229
|
+
configDir: options.configDir || (typeof fileOrObject === 'string' ? getConfigFileDirectory(fileOrObject) : process.cwd())
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
assertCustomResolversAllowed(options.variableSources, this.safetyPolicy)
|
|
233
|
+
assertCustomFunctionsAllowed(options.functions, this.safetyPolicy)
|
|
234
|
+
|
|
182
235
|
// Merge legacy allowUnknownParams and allowUnknownFileRefs into allowUnresolvedVariables
|
|
183
236
|
let unresolvedSetting = this.settings.allowUnresolvedVariables
|
|
184
237
|
if (unresolvedSetting !== true) {
|
|
@@ -197,6 +250,9 @@ class Configorama {
|
|
|
197
250
|
// Paths whose current value is a literal with no variables — skip rebuilding
|
|
198
251
|
// their leaf object on every subsequent populateObjectImpl iteration.
|
|
199
252
|
this._resolvedPaths = new Set()
|
|
253
|
+
// Ignore-path decisions are constant for a run (patterns are fixed per
|
|
254
|
+
// instance), so cache per path to skip repeated glob matching.
|
|
255
|
+
this._ignorePathCache = new Map()
|
|
200
256
|
// Cache raw file contents per absolute path so repeated ${file:...} refs
|
|
201
257
|
// to the same file (e.g., merged twice into different keys) don't reread.
|
|
202
258
|
this._fileContentCache = new Map()
|
|
@@ -208,7 +264,7 @@ class Configorama {
|
|
|
208
264
|
this._needsRawClone = !!(
|
|
209
265
|
this.settings.returnMetadata ||
|
|
210
266
|
this.settings.returnPreResolvedVariableDetails ||
|
|
211
|
-
VERBOSE ||
|
|
267
|
+
VERBOSE || this.setupMode || showFound
|
|
212
268
|
)
|
|
213
269
|
|
|
214
270
|
this.foundVariables = []
|
|
@@ -272,9 +328,10 @@ class Configorama {
|
|
|
272
328
|
// Set configPath for file references
|
|
273
329
|
this.configPath = options.configDir || process.cwd()
|
|
274
330
|
} else if (typeof fileOrObject === 'string') {
|
|
331
|
+
assertSafeConfigInput(fileOrObject, this.safetyPolicy)
|
|
275
332
|
// read and parse file
|
|
276
333
|
const fileContents = fs.readFileSync(fileOrObject, 'utf-8')
|
|
277
|
-
const fileDirectory =
|
|
334
|
+
const fileDirectory = getConfigFileDirectory(fileOrObject)
|
|
278
335
|
const fileType = path.extname(fileOrObject)
|
|
279
336
|
|
|
280
337
|
this.configFilePath = fileOrObject
|
|
@@ -502,9 +559,11 @@ class Configorama {
|
|
|
502
559
|
// Build prefix lookup map for O(1) type detection (perf optimization)
|
|
503
560
|
this._resolverByPrefix = new Map()
|
|
504
561
|
for (const r of this.variableTypes) {
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
562
|
+
const prefixes = r.prefixes || [r.prefix || r.type]
|
|
563
|
+
for (const prefix of prefixes) {
|
|
564
|
+
if (prefix && r.match instanceof RegExp && !r.internal) {
|
|
565
|
+
this._resolverByPrefix.set(prefix + ':', r)
|
|
566
|
+
}
|
|
508
567
|
}
|
|
509
568
|
}
|
|
510
569
|
|
|
@@ -575,6 +634,37 @@ class Configorama {
|
|
|
575
634
|
if (value === undefined || value === null || value === 'null') return ''
|
|
576
635
|
return String(value)
|
|
577
636
|
},
|
|
637
|
+
Array: (value) => {
|
|
638
|
+
if (Array.isArray(value)) return value
|
|
639
|
+
if (typeof value !== 'string') {
|
|
640
|
+
throw new Error(`Configorama Error: Expected Array, got "${value}"`)
|
|
641
|
+
}
|
|
642
|
+
const trimmed = value.trim()
|
|
643
|
+
if (!trimmed) return []
|
|
644
|
+
try {
|
|
645
|
+
const parsed = JSON5.parse(trimmed)
|
|
646
|
+
if (Array.isArray(parsed)) return parsed
|
|
647
|
+
throw new Error('not-array')
|
|
648
|
+
} catch (error) {
|
|
649
|
+
if (trimmed.includes(',')) {
|
|
650
|
+
return trimmed.split(',').map(item => item.trim()).filter(Boolean)
|
|
651
|
+
}
|
|
652
|
+
throw new Error(`Configorama Error: Expected Array, got "${value}"`)
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
Object: (value) => {
|
|
656
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value
|
|
657
|
+
if (typeof value !== 'string') {
|
|
658
|
+
throw new Error(`Configorama Error: Expected Object, got "${value}"`)
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const parsed = JSON5.parse(value)
|
|
662
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed
|
|
663
|
+
} catch (error) {
|
|
664
|
+
// Fall through to consistent error below.
|
|
665
|
+
}
|
|
666
|
+
throw new Error(`Configorama Error: Expected Object, got "${value}"`)
|
|
667
|
+
},
|
|
578
668
|
Json: (value) => {
|
|
579
669
|
try {
|
|
580
670
|
return typeof value === 'string' ? JSON.parse(value) : value
|
|
@@ -588,6 +678,7 @@ class Configorama {
|
|
|
588
678
|
// The helpText argument is extracted during metadata collection for the wizard
|
|
589
679
|
return value
|
|
590
680
|
},
|
|
681
|
+
oneOf: validateOneOf,
|
|
591
682
|
}
|
|
592
683
|
|
|
593
684
|
// Apply user defined filters
|
|
@@ -746,7 +837,7 @@ class Configorama {
|
|
|
746
837
|
dynamicArgs: this.settings.dynamicArgs
|
|
747
838
|
})
|
|
748
839
|
this.configFileContents = ''
|
|
749
|
-
if (VERBOSE || showFoundVariables || this.settings.returnPreResolvedVariableDetails ||
|
|
840
|
+
if (VERBOSE || showFoundVariables || this.settings.returnPreResolvedVariableDetails || this.setupMode) {
|
|
750
841
|
this.configFileContents = fs.readFileSync(this.configFilePath, 'utf8')
|
|
751
842
|
}
|
|
752
843
|
/*
|
|
@@ -789,7 +880,7 @@ class Configorama {
|
|
|
789
880
|
const variableSyntax = this.variableSyntax
|
|
790
881
|
const variablesKnownTypes = this.variablesKnownTypes
|
|
791
882
|
|
|
792
|
-
if (VERBOSE || showFoundVariables || this.settings.returnPreResolvedVariableDetails ||
|
|
883
|
+
if (VERBOSE || showFoundVariables || this.settings.returnPreResolvedVariableDetails || this.setupMode) {
|
|
793
884
|
const metadata = this.collectVariableMetadata()
|
|
794
885
|
|
|
795
886
|
const enrich = await enrichMetadata(
|
|
@@ -844,15 +935,18 @@ class Configorama {
|
|
|
844
935
|
displayConfigurableVariables(displayParams)
|
|
845
936
|
|
|
846
937
|
|
|
847
|
-
// WALK through CLI prompt
|
|
848
|
-
if (
|
|
938
|
+
// WALK through CLI prompt when setup mode is active
|
|
939
|
+
if (this.setupMode) {
|
|
849
940
|
logHeader('Setup Mode')
|
|
850
941
|
// deepLog('enrich', enrich)
|
|
851
942
|
const userInputs = await runConfigWizard(enrich, this.originalConfig, this.configFilePath)
|
|
943
|
+
const setupRequirements = buildConfigRequirements(enrich)
|
|
944
|
+
this.setupRequirements = setupRequirements
|
|
945
|
+
const displayInputs = redactUserInputsByRequirements(userInputs, setupRequirements)
|
|
852
946
|
|
|
853
947
|
logHeader('User Inputs Summary')
|
|
854
948
|
console.log()
|
|
855
|
-
console.log(JSON.stringify(
|
|
949
|
+
console.log(JSON.stringify(displayInputs, null, 2))
|
|
856
950
|
|
|
857
951
|
// TODO set values
|
|
858
952
|
|
|
@@ -902,6 +996,12 @@ class Configorama {
|
|
|
902
996
|
|
|
903
997
|
const useDotEnv = this.originalConfig.useDotenv || this.originalConfig.useDotEnv
|
|
904
998
|
if ((useDotEnv && useDotEnv === true) || this.settings.useDotEnvFiles) {
|
|
999
|
+
if (this.safetyPolicy.blockDotEnv) {
|
|
1000
|
+
throw new ConfigoramaError('blocked_by_safe_mode', 'Dotenv loading is blocked in safe mode', {
|
|
1001
|
+
surface: 'dotenv',
|
|
1002
|
+
configPath: this.configFilePath,
|
|
1003
|
+
})
|
|
1004
|
+
}
|
|
905
1005
|
let providerStage
|
|
906
1006
|
/* has hardcoded stage */
|
|
907
1007
|
if (
|
|
@@ -1127,7 +1227,14 @@ class Configorama {
|
|
|
1127
1227
|
// ## PROPERTY HANDLING ##
|
|
1128
1228
|
// #######################
|
|
1129
1229
|
isIgnorePath(pathValue) {
|
|
1130
|
-
|
|
1230
|
+
if (!this.ignorePathPatterns.length) return false
|
|
1231
|
+
// NUL-join so distinct path arrays can never collide on a shared key.
|
|
1232
|
+
const key = isArray(pathValue) ? pathValue.join('\x00') : String(pathValue)
|
|
1233
|
+
const cached = this._ignorePathCache.get(key)
|
|
1234
|
+
if (cached !== undefined) return cached
|
|
1235
|
+
const result = shouldIgnorePath(pathValue, this.ignorePathPatterns)
|
|
1236
|
+
this._ignorePathCache.set(key, result)
|
|
1237
|
+
return result
|
|
1131
1238
|
}
|
|
1132
1239
|
// True when the value has a configorama-typed token that resolves even inside an
|
|
1133
1240
|
// ignore-path (file/text/env/opt/cron/git/custom) — i.e. not just self/CFN refs.
|
|
@@ -1808,6 +1915,9 @@ class Configorama {
|
|
|
1808
1915
|
valueToPopulate = `"${valueToPopulate}"`
|
|
1809
1916
|
}
|
|
1810
1917
|
}
|
|
1918
|
+
if (isNestedFilterArgument(property, currentMatchedString)) {
|
|
1919
|
+
valueToPopulate = encodeFilterArg(valueToPopulate)
|
|
1920
|
+
}
|
|
1811
1921
|
property = replaceAll(currentMatchedString, valueToPopulate, property)
|
|
1812
1922
|
// console.log('property replaceAll', property)
|
|
1813
1923
|
|
|
@@ -1818,7 +1928,10 @@ class Configorama {
|
|
|
1818
1928
|
// partial replacement, number
|
|
1819
1929
|
} else if (isNumber(valueToPopulate)) {
|
|
1820
1930
|
if (DEBUG_TYPE) console.log('DEBUG_TYPE isNumber')
|
|
1821
|
-
|
|
1931
|
+
const replacementValue = isNestedFilterArgument(property, matchedString)
|
|
1932
|
+
? encodeFilterArg(valueToPopulate)
|
|
1933
|
+
: String(valueToPopulate)
|
|
1934
|
+
property = replaceAll(matchedString, replacementValue, property)
|
|
1822
1935
|
// TODO This was temp fix for array value mismatch from filters. This fixes filterInner: ${commas | split(${self:inner}, 2) }
|
|
1823
1936
|
// } else if (isArray(valueToPopulate) && valueToPopulate.length === 1) {
|
|
1824
1937
|
// property = replaceAll(matchedString, String(valueToPopulate[0]), property)
|
|
@@ -1841,7 +1954,9 @@ class Configorama {
|
|
|
1841
1954
|
)
|
|
1842
1955
|
// Only encode for file() or text() references where JSON braces break regex matching
|
|
1843
1956
|
const isFileOrTextRef = /\bfile\s*\(|\btext\s*\(/.test(property)
|
|
1844
|
-
if (
|
|
1957
|
+
if (isNestedFilterArgument(property, matchedString)) {
|
|
1958
|
+
property = replaceAll(matchedString, encodeFilterArg(valueToPopulate), property)
|
|
1959
|
+
} else if (isNestedInVariable && isFileOrTextRef) {
|
|
1845
1960
|
// Encode object as base64 to avoid breaking variable syntax with nested braces
|
|
1846
1961
|
const encodedObj = encodeJsonForVariable(valueToPopulate)
|
|
1847
1962
|
property = replaceAll(matchedString, encodedObj, property)
|
|
@@ -2069,7 +2184,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2069
2184
|
const rawArgs = funcMatch[2]
|
|
2070
2185
|
if (rawArgs) {
|
|
2071
2186
|
const splitter = splitCsv(rawArgs, ', ')
|
|
2072
|
-
filterArgs = formatFunctionArgs(splitter)
|
|
2187
|
+
filterArgs = formatFunctionArgs(splitter.map(arg => resolveStaticFilterArg(arg, this.config)))
|
|
2073
2188
|
}
|
|
2074
2189
|
}
|
|
2075
2190
|
|
|
@@ -2515,7 +2630,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2515
2630
|
// Parse arguments using the same logic as functions
|
|
2516
2631
|
if (rawArgs) {
|
|
2517
2632
|
const splitter = splitCsv(rawArgs, ', ')
|
|
2518
|
-
filterArgs = formatFunctionArgs(splitter)
|
|
2633
|
+
filterArgs = formatFunctionArgs(splitter.map(arg => resolveStaticFilterArg(arg, this.config)))
|
|
2519
2634
|
}
|
|
2520
2635
|
}
|
|
2521
2636
|
|
|
@@ -2787,7 +2902,8 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2787
2902
|
textRefSyntax: textRefSyntax,
|
|
2788
2903
|
varPrefix: this.varPrefix,
|
|
2789
2904
|
varSuffix: this.varSuffix,
|
|
2790
|
-
fileContentCache: this._fileContentCache
|
|
2905
|
+
fileContentCache: this._fileContentCache,
|
|
2906
|
+
safetyPolicy: this.safetyPolicy
|
|
2791
2907
|
}
|
|
2792
2908
|
return getValueFromFileResolver(ctx, variableString, options)
|
|
2793
2909
|
}
|
|
@@ -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')
|
|
@@ -84,7 +85,7 @@ function parseFileContents(content, filePath) {
|
|
|
84
85
|
if (ext === 'toml' || ext === 'tml') {
|
|
85
86
|
return TOML.parse(content)
|
|
86
87
|
}
|
|
87
|
-
if (ext
|
|
88
|
+
if (isIniLikeExtension(ext)) {
|
|
88
89
|
return INI.parse(content)
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -92,6 +93,10 @@ function parseFileContents(content, filePath) {
|
|
|
92
93
|
return content
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
function isIniLikeExtension(ext) {
|
|
97
|
+
return ext === 'ini' || ext === 'env'
|
|
98
|
+
}
|
|
99
|
+
|
|
95
100
|
/**
|
|
96
101
|
* Resolves a value from a file reference
|
|
97
102
|
* @param {object} ctx - Context object with instance properties
|
|
@@ -109,6 +114,7 @@ function parseFileContents(content, filePath) {
|
|
|
109
114
|
* @param {string} ctx.varPrefix - Variable prefix (e.g., '${')
|
|
110
115
|
* @param {string} ctx.varSuffix - Variable suffix (e.g., '}')
|
|
111
116
|
* @param {Map<string, string>} [ctx.fileContentCache] - Optional per-instance read cache keyed by absolute file path
|
|
117
|
+
* @param {object} [ctx.safetyPolicy] - Optional safe-mode policy for executable and root checks
|
|
112
118
|
* @param {string} variableString - The variable string to resolve
|
|
113
119
|
* @param {object} options - Resolution options
|
|
114
120
|
* @returns {Promise<any>}
|
|
@@ -187,6 +193,9 @@ async function getValueFromFile(ctx, variableString, options) {
|
|
|
187
193
|
}
|
|
188
194
|
|
|
189
195
|
const exists = fs.existsSync(fullFilePath)
|
|
196
|
+
if (ctx.safetyPolicy) {
|
|
197
|
+
checkFileAccess(fullFilePath, ctx.safetyPolicy, { variableString })
|
|
198
|
+
}
|
|
190
199
|
|
|
191
200
|
const fileRefEntry = {
|
|
192
201
|
filePath: fullFilePath,
|
|
@@ -402,7 +411,7 @@ ${JSON.stringify(options.context, null, 2)}`,
|
|
|
402
411
|
if (fileExtension === 'toml' || fileExtension === 'tml') {
|
|
403
412
|
valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
|
|
404
413
|
}
|
|
405
|
-
if (fileExtension
|
|
414
|
+
if (isIniLikeExtension(fileExtension)) {
|
|
406
415
|
valueToPopulate = INI.toJson(valueToPopulate)
|
|
407
416
|
}
|
|
408
417
|
if (fileExtension === 'tf' || fileExtension === 'hcl') {
|
|
@@ -442,7 +451,7 @@ Please use ":" or "." to reference sub properties. ${deepPropertiesStr}`
|
|
|
442
451
|
return Promise.resolve(valueToPopulate)
|
|
443
452
|
}
|
|
444
453
|
|
|
445
|
-
if (fileExtension
|
|
454
|
+
if (isIniLikeExtension(fileExtension)) {
|
|
446
455
|
valueToPopulate = INI.parse(valueToPopulate)
|
|
447
456
|
return Promise.resolve(valueToPopulate)
|
|
448
457
|
}
|
|
@@ -114,6 +114,32 @@ const GIT_KEYS = {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function createResolver(cwd) {
|
|
117
|
+
let gitRepo
|
|
118
|
+
const gitResultCache = new Map()
|
|
119
|
+
|
|
120
|
+
function isCurrentGitRepo() {
|
|
121
|
+
if (typeof gitRepo === 'undefined') {
|
|
122
|
+
gitRepo = isGitRepo(cwd)
|
|
123
|
+
}
|
|
124
|
+
return gitRepo
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function cachedSafeGit(key, cmdFn) {
|
|
128
|
+
if (!gitResultCache.has(key)) {
|
|
129
|
+
gitResultCache.set(key, _safeGit(cmdFn))
|
|
130
|
+
}
|
|
131
|
+
return gitResultCache.get(key)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function gitExec(args) {
|
|
135
|
+
const key = `git:${JSON.stringify(args)}`
|
|
136
|
+
return cachedSafeGit(key, () => _execFile('git', args))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function gitRemote(name = 'origin') {
|
|
140
|
+
return cachedSafeGit(`remote:${name}`, () => getGitRemote(name))
|
|
141
|
+
}
|
|
142
|
+
|
|
117
143
|
async function _getValueFromGit(variableString) {
|
|
118
144
|
const variable = variableString.split(`${GIT_PREFIX}:`)[1]
|
|
119
145
|
let value = null
|
|
@@ -123,14 +149,14 @@ function createResolver(cwd) {
|
|
|
123
149
|
// undefined. This lets fallbacks like `${git:branch, "main"}` work, and
|
|
124
150
|
// when there's no fallback the outer resolver throws a clear "Unable to
|
|
125
151
|
// resolve config variable" error pointing at the config path.
|
|
126
|
-
if (!
|
|
152
|
+
if (!isCurrentGitRepo()) {
|
|
127
153
|
return undefined
|
|
128
154
|
}
|
|
129
155
|
|
|
130
156
|
if (variable.match(/^remote/i)) {
|
|
131
157
|
const hasParams = functionRegex.exec(variableString)
|
|
132
158
|
const remoteName = (hasParams && hasParams[2]) ? formatFunctionArgs(hasParams[2]) : 'origin'
|
|
133
|
-
return
|
|
159
|
+
return gitRemote(remoteName)
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
const normalizedVar = (variable || '').toLowerCase()
|
|
@@ -152,7 +178,7 @@ function createResolver(cwd) {
|
|
|
152
178
|
case 'repository':
|
|
153
179
|
case 'reposlug':
|
|
154
180
|
case 'repo-slug': {
|
|
155
|
-
const urla = await
|
|
181
|
+
const urla = await gitRemote()
|
|
156
182
|
if (!urla) return undefined
|
|
157
183
|
const parseda = GitUrlParse(urla)
|
|
158
184
|
value = parseda.full_name
|
|
@@ -162,7 +188,7 @@ function createResolver(cwd) {
|
|
|
162
188
|
case GIT_KEYS.name:
|
|
163
189
|
case 'reponame': // repoName
|
|
164
190
|
case 'repo-name': {
|
|
165
|
-
const toplevel = await
|
|
191
|
+
const toplevel = await gitExec(['rev-parse', '--show-toplevel'])
|
|
166
192
|
if (!toplevel) return undefined
|
|
167
193
|
value = path.basename(toplevel)
|
|
168
194
|
break
|
|
@@ -173,7 +199,7 @@ function createResolver(cwd) {
|
|
|
173
199
|
case 'organization':
|
|
174
200
|
case 'repoowner': // repoOwner
|
|
175
201
|
case 'repo-owner': {
|
|
176
|
-
const url = await
|
|
202
|
+
const url = await gitRemote()
|
|
177
203
|
if (!url) return undefined
|
|
178
204
|
const parsed = GitUrlParse(url)
|
|
179
205
|
value = parsed.organization || parsed.owner
|
|
@@ -185,12 +211,12 @@ function createResolver(cwd) {
|
|
|
185
211
|
case 'dirpath': // dirPath
|
|
186
212
|
case 'dir-path':
|
|
187
213
|
case 'dir_path': {
|
|
188
|
-
const gitBasePath = await
|
|
214
|
+
const gitBasePath = await gitExec(['rev-parse', '--show-toplevel'])
|
|
189
215
|
if (!gitBasePath) return undefined
|
|
190
216
|
if (cwd) {
|
|
191
217
|
const subPath = cwd.replace(gitBasePath, '')
|
|
192
|
-
const branch = await
|
|
193
|
-
const url = await
|
|
218
|
+
const branch = await gitExec(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
219
|
+
const url = await gitRemote()
|
|
194
220
|
if (!url) return undefined
|
|
195
221
|
value = (subPath && branch) ? `${url}/tree/${branch}${subPath}` : url
|
|
196
222
|
}
|
|
@@ -200,12 +226,12 @@ function createResolver(cwd) {
|
|
|
200
226
|
case GIT_KEYS.url:
|
|
201
227
|
case 'repourl': // repoUrl
|
|
202
228
|
case 'repo-url':
|
|
203
|
-
value = await
|
|
229
|
+
value = await gitRemote()
|
|
204
230
|
break
|
|
205
231
|
// Current commit sha
|
|
206
232
|
case 'sha':
|
|
207
233
|
case 'sha1':
|
|
208
|
-
value = await
|
|
234
|
+
value = await gitExec(['rev-parse', '--short', 'HEAD'])
|
|
209
235
|
break
|
|
210
236
|
// Current commit full sha
|
|
211
237
|
case GIT_KEYS.commit:
|
|
@@ -213,7 +239,7 @@ function createResolver(cwd) {
|
|
|
213
239
|
case 'commit-sha':
|
|
214
240
|
case 'commithash':
|
|
215
241
|
case 'commit-hash':
|
|
216
|
-
value = await
|
|
242
|
+
value = await gitExec(['rev-parse', 'HEAD'])
|
|
217
243
|
break
|
|
218
244
|
// Branches
|
|
219
245
|
case GIT_KEYS.branch:
|
|
@@ -221,7 +247,7 @@ function createResolver(cwd) {
|
|
|
221
247
|
case 'branch-name':
|
|
222
248
|
case 'currentbranch': // currentBranch
|
|
223
249
|
case 'current-branch':
|
|
224
|
-
value = await
|
|
250
|
+
value = await gitExec(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
225
251
|
break
|
|
226
252
|
// Commit msg
|
|
227
253
|
case GIT_KEYS.message:
|
|
@@ -230,26 +256,26 @@ function createResolver(cwd) {
|
|
|
230
256
|
case 'commit-message':
|
|
231
257
|
case 'commitmsg': // commitMsg
|
|
232
258
|
case 'commit-msg':
|
|
233
|
-
value = await
|
|
259
|
+
value = await gitExec(['log', '-1', '--pretty=%B'])
|
|
234
260
|
break
|
|
235
261
|
// Git tags
|
|
236
262
|
case GIT_KEYS.tag:
|
|
237
263
|
case 'describe':
|
|
238
|
-
value = await
|
|
264
|
+
value = await gitExec(['describe', '--always'])
|
|
239
265
|
break
|
|
240
266
|
// Git tags
|
|
241
267
|
case 'describeLight':
|
|
242
268
|
case 'describelight':
|
|
243
269
|
case 'describe-light':
|
|
244
|
-
value = await
|
|
270
|
+
value = await gitExec(['describe', '--always', '--tags'])
|
|
245
271
|
break
|
|
246
272
|
// Is branch dirty
|
|
247
273
|
case 'isDirty':
|
|
248
274
|
case 'isdirty':
|
|
249
275
|
case 'is-dirty': {
|
|
250
|
-
const writeTree = await
|
|
276
|
+
const writeTree = await gitExec(['write-tree'])
|
|
251
277
|
if (!writeTree) return undefined
|
|
252
|
-
const changes = await
|
|
278
|
+
const changes = await gitExec(['diff-index', writeTree.trim(), '--'])
|
|
253
279
|
if (changes === undefined) return undefined
|
|
254
280
|
value = `${changes.length > 0}`
|
|
255
281
|
break
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Resolves values from CLI option flags
|
|
2
|
-
// Matches ${opt:FLAG_NAME} syntax with optional fallback values
|
|
3
|
-
const optRefSyntax = RegExp(/^opt:/g)
|
|
2
|
+
// Matches ${opt:FLAG_NAME} and ${option:FLAG_NAME} syntax with optional fallback values
|
|
3
|
+
const optRefSyntax = RegExp(/^(?:opt|option):/g)
|
|
4
4
|
|
|
5
5
|
function getValueFromOptions(variableString, options) {
|
|
6
6
|
const requestedOption = variableString.split(':')[1]
|
|
@@ -12,8 +12,9 @@ module.exports = {
|
|
|
12
12
|
type: 'options',
|
|
13
13
|
source: 'user',
|
|
14
14
|
prefix: 'opt',
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
prefixes: ['opt', 'option'],
|
|
16
|
+
syntax: '${option:flagName}',
|
|
17
|
+
description: 'Resolves CLI option flags. Examples: ${option:stage}, ${opt:stage}, ${option:other, "fallbackValue"}',
|
|
17
18
|
match: optRefSyntax,
|
|
18
19
|
resolver: getValueFromOptions
|
|
19
20
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const MARKER = '__CONFIGORAMA_FILTER_ARG__'
|
|
2
|
+
|
|
3
|
+
class ResolvedFilterArg {
|
|
4
|
+
constructor(value) {
|
|
5
|
+
this.value = value
|
|
6
|
+
this.__resolvedFilterArg = true
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
toString() {
|
|
10
|
+
return String(this.value)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
valueOf() {
|
|
14
|
+
return this.value
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function encodeBase64Url(value) {
|
|
19
|
+
return Buffer.from(value).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function decodeBase64Url(value) {
|
|
23
|
+
let base64 = value.replace(/-/g, '+').replace(/_/g, '/')
|
|
24
|
+
while (base64.length % 4) base64 += '='
|
|
25
|
+
return Buffer.from(base64, 'base64').toString('utf8')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function encodeFilterArg(value) {
|
|
29
|
+
return `${MARKER}:${encodeBase64Url(JSON.stringify(value))}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isEncodedFilterArg(value) {
|
|
33
|
+
return typeof value === 'string' && value.startsWith(`${MARKER}:`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function decodeFilterArg(value) {
|
|
37
|
+
if (!isEncodedFilterArg(value)) return value
|
|
38
|
+
const encoded = value.slice(MARKER.length + 1)
|
|
39
|
+
return new ResolvedFilterArg(JSON.parse(decodeBase64Url(encoded)))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isResolvedFilterArg(value) {
|
|
43
|
+
return Boolean(value && value.__resolvedFilterArg)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function unwrapFilterArg(value) {
|
|
47
|
+
return isResolvedFilterArg(value) ? value.value : value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
ResolvedFilterArg,
|
|
52
|
+
decodeFilterArg,
|
|
53
|
+
encodeFilterArg,
|
|
54
|
+
isEncodedFilterArg,
|
|
55
|
+
isResolvedFilterArg,
|
|
56
|
+
unwrapFilterArg,
|
|
57
|
+
}
|