configorama 0.11.2 → 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 (52) 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 +117 -15
  12. package/src/resolvers/valueFromEval.js +11 -1
  13. package/src/resolvers/valueFromFile.js +5 -0
  14. package/src/resolvers/valueFromGit.js +43 -17
  15. package/src/resolvers/valueFromOptions.js +5 -4
  16. package/src/utils/filters/filterArgs.js +57 -0
  17. package/src/utils/filters/oneOf.js +77 -0
  18. package/src/utils/introspection/audit.js +78 -0
  19. package/src/utils/introspection/graph.js +43 -0
  20. package/src/utils/introspection/model.js +150 -0
  21. package/src/utils/introspection/model.test.js +93 -0
  22. package/src/utils/parsing/commentAnnotations.js +107 -0
  23. package/src/utils/parsing/commentAnnotations.test.js +123 -0
  24. package/src/utils/parsing/enrichMetadata.js +64 -1
  25. package/src/utils/parsing/enrichMetadata.test.js +84 -0
  26. package/src/utils/parsing/extractComment.js +145 -0
  27. package/src/utils/parsing/extractComment.test.js +182 -0
  28. package/src/utils/parsing/preProcess.js +2 -1
  29. package/src/utils/paths/findLineForKey.js +2 -2
  30. package/src/utils/paths/ignorePaths.js +21 -9
  31. package/src/utils/redaction/redact.js +78 -0
  32. package/src/utils/redaction/redact.test.js +38 -0
  33. package/src/utils/redaction/setupRedaction.js +47 -0
  34. package/src/utils/redaction/setupRedaction.test.js +68 -0
  35. package/src/utils/requirements/configRequirements.js +351 -0
  36. package/src/utils/requirements/configRequirements.test.js +380 -0
  37. package/src/utils/requirements/serializeRequirements.js +120 -0
  38. package/src/utils/requirements/serializeRequirements.test.js +211 -0
  39. package/src/utils/security/evalSafety.js +86 -0
  40. package/src/utils/security/evalSafety.test.js +61 -0
  41. package/src/utils/security/safetyPolicy.js +110 -0
  42. package/src/utils/security/safetyPolicy.test.js +29 -0
  43. package/src/utils/strings/didYouMean.js +70 -0
  44. package/src/utils/strings/didYouMean.test.js +52 -0
  45. package/src/utils/strings/formatFunctionArgs.js +6 -1
  46. package/src/utils/strings/splitByComma.js +5 -0
  47. package/src/utils/ui/configWizard.js +208 -34
  48. package/src/utils/ui/createEditorLink.js +17 -1
  49. package/src/utils/ui/promptDescriptors.js +196 -0
  50. package/src/utils/ui/promptDescriptors.test.js +162 -0
  51. package/src/utils/variables/cleanVariable.js +22 -0
  52. package/src/utils/variables/getVariableType.js +1 -0
@@ -0,0 +1,38 @@
1
+ const { test } = require('uvu')
2
+ const assert = require('uvu/assert')
3
+ const {
4
+ isSensitiveVariable,
5
+ redactObjectByPaths,
6
+ REDACTED_VALUE,
7
+ } = require('./redact')
8
+
9
+ test('isSensitiveVariable detects common secret names', () => {
10
+ assert.is(isSensitiveVariable('API_KEY'), true)
11
+ assert.is(isSensitiveVariable('clientSecret'), true)
12
+ assert.is(isSensitiveVariable('private_key'), true)
13
+ assert.is(isSensitiveVariable('region'), false)
14
+ })
15
+
16
+ test('isSensitiveVariable honors annotation overrides', () => {
17
+ assert.is(isSensitiveVariable('API_KEY', {
18
+ sensitiveEntries: [{ value: false, path: 'apiKey' }]
19
+ }), false)
20
+
21
+ assert.is(isSensitiveVariable('region', {
22
+ sensitiveEntries: [{ value: true, path: 'region' }]
23
+ }), true)
24
+ })
25
+
26
+ test('redactObjectByPaths redacts nested config paths', () => {
27
+ const redacted = redactObjectByPaths({
28
+ service: 'demo',
29
+ secrets: {
30
+ apiKey: 'secret-value'
31
+ }
32
+ }, ['secrets.apiKey'])
33
+
34
+ assert.is(redacted.service, 'demo')
35
+ assert.is(redacted.secrets.apiKey, REDACTED_VALUE)
36
+ })
37
+
38
+ test.run()
@@ -0,0 +1,47 @@
1
+ const dotProp = require('dot-prop')
2
+ const { REDACTED_VALUE, cloneJson } = require('./redact')
3
+
4
+ function inputSectionForRequirement(requirement) {
5
+ if (!requirement) return null
6
+ if (requirement.variableType === 'option') return 'options'
7
+ if (requirement.variableType === 'env') return 'env'
8
+ if (requirement.variableType === 'self') return 'self'
9
+ if (requirement.variableType === 'dotProp') return 'dotProp'
10
+ return null
11
+ }
12
+
13
+ function redactUserInputsByRequirements(userInputs, requirements) {
14
+ const redacted = cloneJson(userInputs || {})
15
+
16
+ for (const requirement of requirements || []) {
17
+ if (!requirement || requirement.sensitive !== true) continue
18
+ const section = inputSectionForRequirement(requirement)
19
+ if (!section || !redacted[section]) continue
20
+ if (Object.prototype.hasOwnProperty.call(redacted[section], requirement.name)) {
21
+ redacted[section][requirement.name] = REDACTED_VALUE
22
+ }
23
+ }
24
+
25
+ return redacted
26
+ }
27
+
28
+ function redactConfigByRequirements(config, requirements) {
29
+ const redacted = cloneJson(config)
30
+
31
+ for (const requirement of requirements || []) {
32
+ if (!requirement || requirement.sensitive !== true) continue
33
+ for (const configPath of requirement.paths || []) {
34
+ if (configPath && dotProp.has(redacted, configPath)) {
35
+ dotProp.set(redacted, configPath, REDACTED_VALUE)
36
+ }
37
+ }
38
+ }
39
+
40
+ return redacted
41
+ }
42
+
43
+ module.exports = {
44
+ REDACTED_VALUE,
45
+ redactConfigByRequirements,
46
+ redactUserInputsByRequirements,
47
+ }
@@ -0,0 +1,68 @@
1
+ const { test } = require('uvu')
2
+ const assert = require('uvu/assert')
3
+ const {
4
+ REDACTED_VALUE,
5
+ redactConfigByRequirements,
6
+ redactUserInputsByRequirements,
7
+ } = require('./setupRedaction')
8
+
9
+ const requirements = [
10
+ {
11
+ name: 'API_KEY',
12
+ variableType: 'env',
13
+ sensitive: true,
14
+ paths: ['secrets.apiKey'],
15
+ },
16
+ {
17
+ name: 'PUBLIC_KEY',
18
+ variableType: 'env',
19
+ sensitive: false,
20
+ paths: ['secrets.publicKey'],
21
+ },
22
+ {
23
+ name: 'stage',
24
+ variableType: 'option',
25
+ sensitive: false,
26
+ paths: ['stage'],
27
+ },
28
+ ]
29
+
30
+ test('redactUserInputsByRequirements redacts only sensitive setup values', () => {
31
+ const inputs = {
32
+ options: { stage: 'prod' },
33
+ env: {
34
+ API_KEY: 'secret-value',
35
+ PUBLIC_KEY: 'public-value',
36
+ }
37
+ }
38
+
39
+ assert.equal(redactUserInputsByRequirements(inputs, requirements), {
40
+ options: { stage: 'prod' },
41
+ env: {
42
+ API_KEY: REDACTED_VALUE,
43
+ PUBLIC_KEY: 'public-value',
44
+ }
45
+ })
46
+ assert.is(inputs.env.API_KEY, 'secret-value')
47
+ })
48
+
49
+ test('redactConfigByRequirements redacts only sensitive resolved config paths', () => {
50
+ const config = {
51
+ stage: 'prod',
52
+ secrets: {
53
+ apiKey: 'secret-value',
54
+ publicKey: 'public-value',
55
+ }
56
+ }
57
+
58
+ assert.equal(redactConfigByRequirements(config, requirements), {
59
+ stage: 'prod',
60
+ secrets: {
61
+ apiKey: REDACTED_VALUE,
62
+ publicKey: 'public-value',
63
+ }
64
+ })
65
+ assert.is(config.secrets.apiKey, 'secret-value')
66
+ })
67
+
68
+ test.run()
@@ -0,0 +1,351 @@
1
+ const { isSensitiveVariable } = require('../redaction/redact')
2
+
3
+ const TYPE_MAP = {
4
+ Boolean: 'boolean',
5
+ String: 'string',
6
+ Number: 'number',
7
+ Array: 'array',
8
+ Object: 'object',
9
+ Json: 'json',
10
+ }
11
+
12
+ const VARIABLE_TYPE_MAP = {
13
+ options: 'option',
14
+ opt: 'option',
15
+ option: 'option',
16
+ 'dot.prop': 'dotProp',
17
+ dotProp: 'dotProp',
18
+ if: 'eval',
19
+ }
20
+
21
+ function normalizeVariableType(variableType) {
22
+ return VARIABLE_TYPE_MAP[variableType] || variableType || 'unknown'
23
+ }
24
+
25
+ function normalizeType(type) {
26
+ if (!type) return 'string'
27
+ return TYPE_MAP[type] || String(type).toLowerCase()
28
+ }
29
+
30
+ function cleanDefaultValue(value) {
31
+ if (value === undefined || value === null) return null
32
+ if (typeof value !== 'string') return value
33
+ const match = value.match(/^(['"])([\s\S]*)\1$/)
34
+ return match ? match[2] : value
35
+ }
36
+
37
+ function unique(values) {
38
+ return [...new Set(values.filter(value => value !== undefined && value !== null))]
39
+ }
40
+
41
+ function uniqueBy(values, getKey) {
42
+ const seen = new Set()
43
+ const result = []
44
+ for (const value of values) {
45
+ const key = getKey(value)
46
+ if (seen.has(key)) continue
47
+ seen.add(key)
48
+ result.push(value)
49
+ }
50
+ return result
51
+ }
52
+
53
+ function firstValue(values) {
54
+ return values.find(value => value !== undefined && value !== null)
55
+ }
56
+
57
+ function getRequirementName(variable, variableType) {
58
+ const normalizedType = normalizeVariableType(variableType)
59
+ if (!variable) return ''
60
+
61
+ if (normalizedType === 'option') return variable.replace(/^(opt|option|options):/, '')
62
+ if (normalizedType === 'env') return variable.replace(/^env:/, '')
63
+ if (normalizedType === 'param') return variable.replace(/^param:/, '')
64
+ if (normalizedType === 'self') return variable.replace(/^self:/, '')
65
+ if (normalizedType === 'file' || normalizedType === 'text') {
66
+ const match = variable.match(/^(?:file|text)\((.+?)\)/)
67
+ return match ? match[1] : variable
68
+ }
69
+ return variable
70
+ }
71
+
72
+ function getOccurrenceTypes(occurrences) {
73
+ return unique((occurrences || []).map(occ => occ.type))
74
+ }
75
+
76
+ function getOccurrenceTypeEntries(occurrences) {
77
+ return (occurrences || [])
78
+ .filter(occ => occ.type)
79
+ .map(occ => ({
80
+ value: normalizeType(occ.type),
81
+ path: occ.path,
82
+ }))
83
+ }
84
+
85
+ function getDescriptionCandidates(entry, occurrences) {
86
+ const candidates = []
87
+ if (entry.description) {
88
+ candidates.push({
89
+ value: entry.description,
90
+ source: entry.descriptionSource || 'help',
91
+ path: null,
92
+ index: candidates.length,
93
+ })
94
+ }
95
+ for (const occ of occurrences || []) {
96
+ if (!occ.description) continue
97
+ candidates.push({
98
+ value: occ.description,
99
+ source: occ.descriptionSource || 'help',
100
+ path: occ.path,
101
+ index: candidates.length,
102
+ })
103
+ }
104
+ if (entry.descriptions && entry.descriptions.length) {
105
+ const occurrenceDescriptions = new Set(candidates.map(candidate => candidate.value))
106
+ for (const description of entry.descriptions) {
107
+ if (occurrenceDescriptions.has(description)) continue
108
+ candidates.push({
109
+ value: description,
110
+ source: 'help',
111
+ path: null,
112
+ index: candidates.length,
113
+ })
114
+ }
115
+ }
116
+ return uniqueBy(candidates, candidate => `${candidate.source}:${candidate.value}:${candidate.path || ''}`)
117
+ }
118
+
119
+ function getDescriptionPriority(source) {
120
+ if (source === 'commentTag') return -1
121
+ if (source === 'help') return 0
122
+ if (source === 'inlineComment' || source === 'comment') return 1
123
+ if (source === 'leadingComment') return 2
124
+ return 3
125
+ }
126
+
127
+ function selectDescription(candidates) {
128
+ const sorted = [...candidates].sort((a, b) => {
129
+ const priority = getDescriptionPriority(a.source) - getDescriptionPriority(b.source)
130
+ if (priority !== 0) return priority
131
+ return a.index - b.index
132
+ })
133
+ return sorted[0] || null
134
+ }
135
+
136
+ function getOccurrenceDefaults(entry, occurrences) {
137
+ const defaults = (occurrences || []).map(occ => occ.defaultValue)
138
+ if (entry.resolvedValue !== undefined) defaults.push(entry.resolvedValue)
139
+ return defaults
140
+ }
141
+
142
+ function getOccurrenceDefaultEntries(entry, occurrences) {
143
+ const entries = (occurrences || [])
144
+ .filter(occ => occ.defaultValue !== undefined && occ.defaultValue !== null)
145
+ .map(occ => ({
146
+ value: cleanDefaultValue(occ.defaultValue),
147
+ path: occ.path,
148
+ }))
149
+ if (entry.resolvedValue !== undefined && entry.resolvedValue !== null) {
150
+ entries.push({
151
+ value: cleanDefaultValue(entry.resolvedValue),
152
+ path: null,
153
+ })
154
+ }
155
+ return entries
156
+ }
157
+
158
+ function getAllowedValues(entry, occurrences) {
159
+ if (entry.allowedValues) return entry.allowedValues
160
+ const values = (occurrences || []).flatMap(occ => occ.allowedValues || [])
161
+ return values.length ? unique(values).map(String) : null
162
+ }
163
+
164
+ function collectFieldEntries(entry, occurrences, field) {
165
+ const entries = []
166
+ if (entry[field] !== undefined && entry[field] !== null) {
167
+ entries.push({
168
+ value: entry[field],
169
+ path: null,
170
+ })
171
+ }
172
+ for (const occ of occurrences || []) {
173
+ if (occ[field] === undefined || occ[field] === null) continue
174
+ entries.push({
175
+ value: occ[field],
176
+ path: occ.path,
177
+ })
178
+ }
179
+ return entries
180
+ }
181
+
182
+ function selectFieldValue(entries) {
183
+ const entry = (entries || []).find(item => item.value !== undefined && item.value !== null)
184
+ return entry ? entry.value : null
185
+ }
186
+
187
+ function mergeExamples(entry, occurrences) {
188
+ const values = []
189
+ if (entry.examples) values.push(...entry.examples)
190
+ for (const occ of occurrences || []) {
191
+ if (occ.examples) values.push(...occ.examples)
192
+ }
193
+ const merged = unique(values.map(String))
194
+ return merged.length ? merged : null
195
+ }
196
+
197
+ function collectScalarAnnotationConflicts(fieldEntriesByField) {
198
+ const conflicts = []
199
+ for (const [field, entries] of Object.entries(fieldEntriesByField)) {
200
+ const uniqueEntries = uniqueBy(entries, entry => String(entry.value))
201
+ if (uniqueEntries.length > 1) {
202
+ conflicts.push(makeConflict(field, uniqueEntries))
203
+ }
204
+ }
205
+ return conflicts
206
+ }
207
+
208
+ function normalizeAllowedSet(values) {
209
+ return (values || []).map(String).sort()
210
+ }
211
+
212
+ function allowedSetKey(values) {
213
+ return JSON.stringify(normalizeAllowedSet(values))
214
+ }
215
+
216
+ function collectAllowedSets(entry, occurrences) {
217
+ const sets = []
218
+ if (entry.allowedValues) {
219
+ sets.push({ values: entry.allowedValues, path: null })
220
+ }
221
+ for (const occ of occurrences || []) {
222
+ if (occ.allowedValues) {
223
+ sets.push({ values: occ.allowedValues, path: occ.path })
224
+ }
225
+ }
226
+ return sets
227
+ }
228
+
229
+ function getSourceClass(entry) {
230
+ return entry.variableSourceType || entry.sourceClass || null
231
+ }
232
+
233
+ function makeConflict(field, values) {
234
+ return {
235
+ field,
236
+ values,
237
+ paths: unique(values.flatMap(value => value.paths || (value.path ? [value.path] : []))),
238
+ }
239
+ }
240
+
241
+ function collectConflicts({ typeEntries, defaultEntries, allowedSets, scalarAnnotationEntries = {} }) {
242
+ const conflicts = []
243
+
244
+ const typedValues = uniqueBy(typeEntries, entry => entry.value)
245
+ if (typedValues.length > 1) {
246
+ conflicts.push(makeConflict('type', typedValues))
247
+ }
248
+
249
+ const defaultValues = uniqueBy(defaultEntries.filter(entry => entry.value !== null), entry => String(entry.value))
250
+ if (defaultValues.length > 1) {
251
+ conflicts.push(makeConflict('default', defaultValues))
252
+ }
253
+
254
+ const uniqueAllowedSets = uniqueBy(allowedSets, set => allowedSetKey(set.values))
255
+ if (uniqueAllowedSets.length > 1) {
256
+ conflicts.push(makeConflict('allowedValues', uniqueAllowedSets.map(set => ({
257
+ value: normalizeAllowedSet(set.values),
258
+ path: set.path,
259
+ }))))
260
+ }
261
+
262
+ conflicts.push(...collectScalarAnnotationConflicts(scalarAnnotationEntries))
263
+
264
+ return conflicts
265
+ }
266
+
267
+ function getSensitiveValue(name, sensitiveEntries) {
268
+ return isSensitiveVariable(name, { sensitiveEntries })
269
+ }
270
+
271
+ function buildRequirement(varKey, entry) {
272
+ const occurrences = entry.occurrences || []
273
+ const variableType = normalizeVariableType(entry.variableType)
274
+ const name = getRequirementName(entry.variable || varKey, entry.variableType)
275
+ const types = getOccurrenceTypes(occurrences)
276
+ const typeEntries = getOccurrenceTypeEntries(occurrences)
277
+ const descriptionCandidates = getDescriptionCandidates(entry, occurrences)
278
+ const selectedDescription = selectDescription(descriptionCandidates)
279
+ const defaults = getOccurrenceDefaults(entry, occurrences)
280
+ const defaultEntries = getOccurrenceDefaultEntries(entry, occurrences)
281
+ const defaultValue = cleanDefaultValue(firstValue(defaults))
282
+ const allowedSets = collectAllowedSets(entry, occurrences)
283
+ const obtainHintEntries = collectFieldEntries(entry, occurrences, 'obtainHint')
284
+ const defaultHintEntries = collectFieldEntries(entry, occurrences, 'defaultHint')
285
+ const groupEntries = collectFieldEntries(entry, occurrences, 'group')
286
+ const deprecationEntries = collectFieldEntries(entry, occurrences, 'deprecationMessage')
287
+ const sensitiveEntries = collectFieldEntries(entry, occurrences, 'sensitive')
288
+ const conflicts = collectConflicts({
289
+ typeEntries,
290
+ defaultEntries,
291
+ allowedSets,
292
+ scalarAnnotationEntries: {
293
+ obtainHint: obtainHintEntries,
294
+ defaultHint: defaultHintEntries,
295
+ group: groupEntries,
296
+ deprecationMessage: deprecationEntries,
297
+ sensitive: sensitiveEntries,
298
+ },
299
+ })
300
+ const obtainHint = selectFieldValue(obtainHintEntries)
301
+ const defaultHint = selectFieldValue(defaultHintEntries)
302
+ const group = selectFieldValue(groupEntries)
303
+ const deprecationMessage = selectFieldValue(deprecationEntries)
304
+ const sensitive = getSensitiveValue(name, sensitiveEntries)
305
+ const sensitiveSource = sensitiveEntries.length ? 'commentTag' : null
306
+
307
+ const paths = unique(occurrences.map(occ => occ.path))
308
+ const required = occurrences.some(occ => occ.isRequired === true) && defaultValue === null
309
+ const description = selectedDescription ? selectedDescription.value : null
310
+
311
+ return {
312
+ name,
313
+ variable: entry.variable || varKey,
314
+ variableType,
315
+ sourceClass: getSourceClass(entry),
316
+ type: normalizeType(firstValue(types)),
317
+ description,
318
+ descriptionSource: selectedDescription ? selectedDescription.source : null,
319
+ allowedValues: getAllowedValues(entry, occurrences),
320
+ sensitive,
321
+ sensitiveSource,
322
+ required,
323
+ default: defaultValue,
324
+ defaultHint,
325
+ obtainHint,
326
+ examples: mergeExamples(entry, occurrences),
327
+ group,
328
+ deprecationMessage,
329
+ fileExists: entry.fileExists,
330
+ innerVariables: entry.innerVariables || [],
331
+ paths,
332
+ conflicts,
333
+ occurrences,
334
+ }
335
+ }
336
+
337
+ function buildConfigRequirements(analysis) {
338
+ const uniqueVariables = analysis && analysis.uniqueVariables ? analysis.uniqueVariables : {}
339
+ return Object.entries(uniqueVariables).map(([varKey, entry]) => {
340
+ return buildRequirement(varKey, entry)
341
+ })
342
+ }
343
+
344
+ module.exports = {
345
+ buildConfigRequirements,
346
+ buildRequirement,
347
+ cleanDefaultValue,
348
+ collectConflicts,
349
+ normalizeType,
350
+ normalizeVariableType,
351
+ }