configorama 1.0.2 → 1.2.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.
Files changed (129) hide show
  1. package/README.md +41 -1
  2. package/cli.js +14 -1
  3. package/package.json +18 -6
  4. package/plugins/cloudformation/README.md +79 -0
  5. package/plugins/cloudformation/credentials.js +231 -0
  6. package/plugins/cloudformation/index.js +204 -0
  7. package/plugins/cloudformation/package-lock.json +657 -0
  8. package/plugins/cloudformation/package.json +24 -0
  9. package/plugins/onepassword/README.md +175 -0
  10. package/plugins/onepassword/fields.js +126 -0
  11. package/plugins/onepassword/index.js +356 -0
  12. package/plugins/onepassword/normalize.js +157 -0
  13. package/plugins/onepassword/op-cli.js +117 -0
  14. package/plugins/onepassword/package.json +15 -0
  15. package/plugins/onepassword/parser.js +70 -0
  16. package/plugins/onepassword/sync-factory.js +15 -0
  17. package/src/errors.js +3 -0
  18. package/src/index.js +27 -1
  19. package/src/main.js +24 -32
  20. package/src/parsers/dotenv.js +13 -0
  21. package/src/resolvers/valueFromFile.js +2 -1
  22. package/src/sync.js +27 -0
  23. package/src/utils/PromiseTracker.js +11 -4
  24. package/src/utils/PromiseTracker.test.js +48 -0
  25. package/src/utils/introspection/audit.js +39 -9
  26. package/src/utils/parsing/detectFormat.js +70 -0
  27. package/src/utils/parsing/detectFormat.test.js +68 -0
  28. package/src/utils/parsing/parse.js +11 -37
  29. package/src/utils/paths/fileType.js +27 -0
  30. package/src/utils/paths/fileType.test.js +41 -0
  31. package/src/utils/paths/findLineForKey.js +24 -1
  32. package/src/utils/paths/findLineForKey.test.js +20 -1
  33. package/src/utils/setup/applyAnswers.js +41 -0
  34. package/src/utils/setup/setupEngine.js +85 -0
  35. package/src/utils/setup/stdoutRedirect.js +48 -0
  36. package/src/utils/setup/writeAnswers.js +50 -0
  37. package/src/utils/setup/writeDotenv.js +142 -0
  38. package/src/utils/ui/configWizard.js +20 -1
  39. package/types/src/capabilities.d.ts +8 -0
  40. package/types/src/capabilities.d.ts.map +1 -0
  41. package/types/src/display.d.ts +62 -0
  42. package/types/src/display.d.ts.map +1 -0
  43. package/types/src/errors.d.ts +20 -0
  44. package/types/src/errors.d.ts.map +1 -0
  45. package/types/src/index.d.ts +119 -0
  46. package/types/src/index.d.ts.map +1 -1
  47. package/types/src/main.d.ts +23 -16
  48. package/types/src/main.d.ts.map +1 -1
  49. package/types/src/metadata.d.ts +28 -0
  50. package/types/src/metadata.d.ts.map +1 -0
  51. package/types/src/parsers/dotenv.d.ts +6 -0
  52. package/types/src/parsers/dotenv.d.ts.map +1 -0
  53. package/types/src/parsers/esm.d.ts.map +1 -1
  54. package/types/src/parsers/typescript.d.ts.map +1 -1
  55. package/types/src/resolvers/valueFromCron.d.ts.map +1 -1
  56. package/types/src/resolvers/valueFromEval.d.ts.map +1 -1
  57. package/types/src/resolvers/valueFromFile.d.ts +4 -0
  58. package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
  59. package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
  60. package/types/src/resolvers/valueFromOptions.d.ts +1 -0
  61. package/types/src/resolvers/valueFromOptions.d.ts.map +1 -1
  62. package/types/src/sync.d.ts.map +1 -1
  63. package/types/src/utils/BoundedMap.d.ts +10 -0
  64. package/types/src/utils/BoundedMap.d.ts.map +1 -0
  65. package/types/src/utils/PromiseTracker.d.ts +1 -1
  66. package/types/src/utils/PromiseTracker.d.ts.map +1 -1
  67. package/types/src/utils/filters/filterArgs.d.ts +13 -0
  68. package/types/src/utils/filters/filterArgs.d.ts.map +1 -0
  69. package/types/src/utils/filters/oneOf.d.ts +7 -0
  70. package/types/src/utils/filters/oneOf.d.ts.map +1 -0
  71. package/types/src/utils/introspection/audit.d.ts +14 -0
  72. package/types/src/utils/introspection/audit.d.ts.map +1 -0
  73. package/types/src/utils/introspection/graph.d.ts +4 -0
  74. package/types/src/utils/introspection/graph.d.ts.map +1 -0
  75. package/types/src/utils/introspection/model.d.ts +43 -0
  76. package/types/src/utils/introspection/model.d.ts.map +1 -0
  77. package/types/src/utils/parsing/commentAnnotations.d.ts +18 -0
  78. package/types/src/utils/parsing/commentAnnotations.d.ts.map +1 -0
  79. package/types/src/utils/parsing/detectFormat.d.ts +17 -0
  80. package/types/src/utils/parsing/detectFormat.d.ts.map +1 -0
  81. package/types/src/utils/parsing/enrichMetadata.d.ts.map +1 -1
  82. package/types/src/utils/parsing/extractComment.d.ts +20 -0
  83. package/types/src/utils/parsing/extractComment.d.ts.map +1 -0
  84. package/types/src/utils/parsing/parse.d.ts.map +1 -1
  85. package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
  86. package/types/src/utils/paths/fileType.d.ts +15 -0
  87. package/types/src/utils/paths/fileType.d.ts.map +1 -0
  88. package/types/src/utils/paths/findLineForKey.d.ts +9 -0
  89. package/types/src/utils/paths/findLineForKey.d.ts.map +1 -1
  90. package/types/src/utils/paths/ignorePaths.d.ts +5 -0
  91. package/types/src/utils/paths/ignorePaths.d.ts.map +1 -0
  92. package/types/src/utils/redaction/redact.d.ts +10 -0
  93. package/types/src/utils/redaction/redact.d.ts.map +1 -0
  94. package/types/src/utils/redaction/setupRedaction.d.ts +5 -0
  95. package/types/src/utils/redaction/setupRedaction.d.ts.map +1 -0
  96. package/types/src/utils/requirements/configRequirements.d.ts +70 -0
  97. package/types/src/utils/requirements/configRequirements.d.ts.map +1 -0
  98. package/types/src/utils/requirements/serializeRequirements.d.ts +63 -0
  99. package/types/src/utils/requirements/serializeRequirements.d.ts.map +1 -0
  100. package/types/src/utils/security/dotenvFileRefs.d.ts +13 -0
  101. package/types/src/utils/security/dotenvFileRefs.d.ts.map +1 -0
  102. package/types/src/utils/security/evalSafety.d.ts +17 -0
  103. package/types/src/utils/security/evalSafety.d.ts.map +1 -0
  104. package/types/src/utils/security/safetyPolicy.d.ts +15 -0
  105. package/types/src/utils/security/safetyPolicy.d.ts.map +1 -0
  106. package/types/src/utils/setup/applyAnswers.d.ts +34 -0
  107. package/types/src/utils/setup/applyAnswers.d.ts.map +1 -0
  108. package/types/src/utils/setup/setupEngine.d.ts +89 -0
  109. package/types/src/utils/setup/setupEngine.d.ts.map +1 -0
  110. package/types/src/utils/setup/stdoutRedirect.d.ts +10 -0
  111. package/types/src/utils/setup/stdoutRedirect.d.ts.map +1 -0
  112. package/types/src/utils/setup/writeAnswers.d.ts +17 -0
  113. package/types/src/utils/setup/writeAnswers.d.ts.map +1 -0
  114. package/types/src/utils/setup/writeDotenv.d.ts +27 -0
  115. package/types/src/utils/setup/writeDotenv.d.ts.map +1 -0
  116. package/types/src/utils/strings/didYouMean.d.ts +23 -0
  117. package/types/src/utils/strings/didYouMean.d.ts.map +1 -0
  118. package/types/src/utils/strings/formatFunctionArgs.d.ts.map +1 -1
  119. package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
  120. package/types/src/utils/strings/splitByComma.d.ts.map +1 -1
  121. package/types/src/utils/ui/configWizard.d.ts +51 -7
  122. package/types/src/utils/ui/configWizard.d.ts.map +1 -1
  123. package/types/src/utils/ui/createEditorLink.d.ts +7 -0
  124. package/types/src/utils/ui/createEditorLink.d.ts.map +1 -1
  125. package/types/src/utils/ui/promptDescriptors.d.ts +39 -0
  126. package/types/src/utils/ui/promptDescriptors.d.ts.map +1 -0
  127. package/types/src/utils/variables/cleanVariable.d.ts.map +1 -1
  128. package/types/src/utils/variables/getVariableType.d.ts.map +1 -1
  129. package/types/src/utils/variables/variableUtils.d.ts +1 -1
@@ -0,0 +1,157 @@
1
+ /* Normalizes 1Password references into canonical secretRef/item/privateLink forms
2
+ Validates aliases, parses private item links, rejects public share links */
3
+
4
+ const ALIAS_PATTERN = /^[A-Za-z0-9_]+$/
5
+ // 1Password item and vault IDs are 26-character lowercase base32 strings.
6
+ const ITEM_ID_PATTERN = /^[a-z0-9]{26}$/
7
+ const PRIVATE_LINK_PREFIXES = ['https://start.1password.com/open/i', 'onepassword://open/i']
8
+
9
+ /**
10
+ * Whether a string has the shape of a 1Password item ID (26-char base32).
11
+ * Used to let colon syntax accept a bare item ID where an alias would go.
12
+ * @param {string} value - Candidate string
13
+ * @returns {boolean} True when it looks like an item ID
14
+ */
15
+ function isItemId(value) {
16
+ return ITEM_ID_PATTERN.test(value)
17
+ }
18
+
19
+ /**
20
+ * Validate an alias name from the refs config.
21
+ * Dots are reserved as the key path separator so they cannot appear in aliases.
22
+ * @param {string} alias - Alias name to validate
23
+ */
24
+ function validateAliasName(alias) {
25
+ if (!ALIAS_PATTERN.test(alias)) {
26
+ throw new Error(`Invalid 1Password alias "${alias}". Aliases may contain only letters, numbers, and underscores.`)
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Check whether a string is a private item link.
32
+ * @param {string} value - Candidate string
33
+ * @returns {boolean} True when the string is a private item link
34
+ */
35
+ function isPrivateLink(value) {
36
+ return PRIVATE_LINK_PREFIXES.some((prefix) => value.startsWith(prefix))
37
+ }
38
+
39
+ /**
40
+ * Recognize abbreviated private-link forms where the scheme, host, and/or
41
+ * path have been stripped, leaving the query params (e.g. "open/i?...&i=ID",
42
+ * or a bare "v=VAULT&i=ID"). Requires an "i=" param plus a 1Password marker
43
+ * or a pure query string, so ordinary item names and unrelated URLs are not
44
+ * misread as links.
45
+ * @param {string} value - Candidate string
46
+ * @returns {boolean} True when the string looks like private-link query params
47
+ */
48
+ function looksLikeLinkParams(value) {
49
+ const query = value.includes('?') ? value.slice(value.indexOf('?') + 1) : value
50
+ if (!/(?:^|&)i=[^&]/.test(query)) return false
51
+ return /1password\.com/.test(value) || /(?:^|\/)open\/i/.test(value) || !value.includes('/')
52
+ }
53
+
54
+ /**
55
+ * Parse a "Copy Private Link" URL (or an abbreviated form) into item and
56
+ * vault IDs. The a (account) and h (host) params identify the account and are
57
+ * intentionally dropped; item + vault IDs are all op item get needs.
58
+ * @param {string} url - Private item link or its query params
59
+ * @returns {{kind: string, item: string, vault: string|undefined, warnings: Array<{code: string, message: string}>}} Normalized reference
60
+ */
61
+ function parsePrivateLink(url) {
62
+ const query = url.includes('?') ? url.slice(url.indexOf('?') + 1) : url
63
+ const params = new URLSearchParams(query)
64
+ const item = params.get('i')
65
+ const vault = params.get('v')
66
+
67
+ if (!item) {
68
+ throw new Error('Invalid 1Password private link. Expected query parameter "i" with the item ID.')
69
+ }
70
+
71
+ const warnings = []
72
+ if (!vault) {
73
+ warnings.push({
74
+ level: 'warning',
75
+ code: 'op_private_link_missing_vault',
76
+ message: '1Password private link did not include a vault ID; service accounts and duplicate item names may require vault scoping.',
77
+ })
78
+ }
79
+
80
+ return { kind: 'privateLink', item, vault: vault || undefined, warnings }
81
+ }
82
+
83
+ /**
84
+ * Normalize a string or object ref value into a canonical reference.
85
+ * Accepted forms: op:// string, item name string, private link string,
86
+ * { item, vault, section, field }, { ref }, { url }.
87
+ * @param {string|object} value - Ref value from config or direct spec
88
+ * @returns {object} Normalized reference ({ kind: 'secretRef'|'item'|'privateLink', ... })
89
+ */
90
+ function normalizeRefValue(value) {
91
+ if (typeof value === 'string') {
92
+ return normalizeStringRef(value)
93
+ }
94
+ if (value && typeof value === 'object') {
95
+ return normalizeObjectRef(value)
96
+ }
97
+ throw new Error(`Invalid 1Password reference. Expected a string or object, got ${typeof value}.`)
98
+ }
99
+
100
+ /**
101
+ * @param {string} value - String ref (secret ref, link, or item name)
102
+ * @returns {object} Normalized reference
103
+ */
104
+ function normalizeStringRef(value) {
105
+ if (value.startsWith('op://')) {
106
+ return { kind: 'secretRef', ref: value }
107
+ }
108
+ // Public share links are rejected in any form (full URL or share token).
109
+ if (value.includes('share.1password.com') || /1password\.com\/s#/.test(value)) {
110
+ throw new Error('Public 1Password share links are not supported. Use Copy Private Link or an op:// secret reference.')
111
+ }
112
+ // Private links: full URL, onepassword://, or an abbreviated form carrying
113
+ // the query params (scheme/host/path stripped).
114
+ if (isPrivateLink(value) || looksLikeLinkParams(value)) {
115
+ return parsePrivateLink(value)
116
+ }
117
+ if (/^https?:\/\/|^onepassword:\/\//.test(value)) {
118
+ throw new Error('Unrecognized 1Password link. Use Copy Private Link or an op:// secret reference.')
119
+ }
120
+ return { kind: 'item', item: value, vault: undefined, section: undefined, field: undefined }
121
+ }
122
+
123
+ /**
124
+ * @param {object} value - Object ref ({ item }, { ref }, or { url })
125
+ * @returns {object} Normalized reference
126
+ */
127
+ function normalizeObjectRef(value) {
128
+ const specifiers = ['item', 'ref', 'url'].filter((key) => value[key] !== undefined)
129
+ if (specifiers.length !== 1) {
130
+ throw new Error('Invalid 1Password reference. Object refs must specify exactly one of "item", "ref", or "url".')
131
+ }
132
+
133
+ if (value.ref !== undefined) {
134
+ return normalizeStringRef(value.ref)
135
+ }
136
+ if (value.url !== undefined) {
137
+ const normalized = normalizeStringRef(value.url)
138
+ if (normalized.kind !== 'privateLink') {
139
+ throw new Error('Invalid 1Password reference. "url" must be a private item link.')
140
+ }
141
+ return normalized
142
+ }
143
+
144
+ if (value.section !== undefined && value.field === undefined) {
145
+ throw new Error('Invalid 1Password reference. "section" is only meaningful together with "field".')
146
+ }
147
+
148
+ return {
149
+ kind: 'item',
150
+ item: value.item,
151
+ vault: value.vault,
152
+ section: value.section,
153
+ field: value.field,
154
+ }
155
+ }
156
+
157
+ module.exports = { validateAliasName, normalizeRefValue, parsePrivateLink, isPrivateLink, isItemId }
@@ -0,0 +1,117 @@
1
+ /* Executes the 1Password CLI with execFile and sanitized error translation
2
+ Never logs stdout/stderr; never passes resolved secret values as arguments */
3
+ const childProcess = require('child_process')
4
+
5
+ const AUTH_ERROR_PATTERN = /sign ?in|session|authoriz|authenticat|not currently signed|locked|service account token/i
6
+ const NOT_FOUND_PATTERN = /isn'?t an? (item|vault)|not found|no item|no vault|doesn'?t exist|more than one item/i
7
+
8
+ /**
9
+ * Run the op binary with the given args.
10
+ * No `command -v op` preflight: translating ENOENT is the existence check.
11
+ * @param {string[]} args - CLI arguments
12
+ * @param {object} [options] - { execFile, opPath, account, configDir, subject }
13
+ * @returns {Promise<string>} stdout
14
+ */
15
+ function runOp(args, options = {}) {
16
+ const execFile = options.execFile || childProcess.execFile
17
+ const binary = options.opPath || 'op'
18
+ const finalArgs = args.slice()
19
+ if (options.account) {
20
+ finalArgs.push('--account', options.account)
21
+ }
22
+ if (options.configDir) {
23
+ finalArgs.push('--config', options.configDir)
24
+ }
25
+
26
+ return new Promise((resolve, reject) => {
27
+ execFile(binary, finalArgs, { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
28
+ if (err) {
29
+ return reject(translateError(err, stderr, options.subject))
30
+ }
31
+ resolve(stdout)
32
+ })
33
+ })
34
+ }
35
+
36
+ /**
37
+ * Translate op failures into sanitized, actionable errors.
38
+ * stderr is inspected for classification but never included in messages.
39
+ * @param {Error & {code?: string|number}} err - execFile error
40
+ * @param {string} stderr - Captured stderr (classification only)
41
+ * @param {string} [subject] - Item/ref identifier already present in user config
42
+ * @returns {Error} Sanitized error
43
+ */
44
+ function translateError(err, stderr, subject) {
45
+ if (err.code === 'ENOENT') {
46
+ return new Error('1Password CLI "op" was not found on PATH. Install 1Password CLI or remove the op resolver.')
47
+ }
48
+ const detail = String(stderr || err.message || '')
49
+ if (AUTH_ERROR_PATTERN.test(detail)) {
50
+ return new Error('1Password CLI could not read the item. Run op signin, unlock 1Password app integration, or configure OP_SERVICE_ACCOUNT_TOKEN.')
51
+ }
52
+ if (NOT_FOUND_PATTERN.test(detail)) {
53
+ const what = subject ? `"${subject}"` : 'the requested item, vault, or field'
54
+ return new Error(`1Password item, vault, or field could not be found (${what}).`)
55
+ }
56
+ return new Error('1Password CLI command failed.')
57
+ }
58
+
59
+ /**
60
+ * Reject config-controlled values that would be parsed as an op flag.
61
+ * execFile takes no shell, so this is not command injection - but a
62
+ * leading "-" turns a positional/vault value into an option that changes
63
+ * op's behavior. Secret refs are exempt: they always start with "op://".
64
+ * @param {string} value - Value destined for the op argument list
65
+ * @param {string} label - Human label for the error message
66
+ */
67
+ function assertNotFlag(value, label) {
68
+ if (typeof value === 'string' && value.startsWith('-')) {
69
+ throw new Error(`Invalid 1Password ${label} "${value}": ${label} cannot start with "-" (it would be read as an op flag). Use the item ID or an op:// secret reference.`)
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Read a secret reference with op read.
75
+ * @param {string} ref - op://vault/item/field reference
76
+ * @param {object} [options] - { execFile, account, configDir }
77
+ * @returns {Promise<string>} Secret value
78
+ */
79
+ function readSecretRef(ref, options = {}) {
80
+ return runOp(['read', '--no-newline', ref], {
81
+ execFile: options.execFile,
82
+ opPath: options.opPath,
83
+ account: options.account,
84
+ configDir: options.configDir,
85
+ subject: ref,
86
+ })
87
+ }
88
+
89
+ /**
90
+ * Fetch an item as JSON with op item get.
91
+ * --reveal is required to include concealed field values in the JSON.
92
+ * @param {string} spec - Item ID or item name
93
+ * @param {object} [options] - { execFile, account, configDir, vault }
94
+ * @returns {Promise<object>} Parsed item JSON
95
+ */
96
+ async function getItem(spec, options = {}) {
97
+ assertNotFlag(spec, 'item')
98
+ const args = ['item', 'get', spec, '--format', 'json', '--reveal']
99
+ if (options.vault) {
100
+ assertNotFlag(options.vault, 'vault')
101
+ args.push('--vault', options.vault)
102
+ }
103
+ const stdout = await runOp(args, {
104
+ execFile: options.execFile,
105
+ opPath: options.opPath,
106
+ account: options.account,
107
+ configDir: options.configDir,
108
+ subject: spec,
109
+ })
110
+ try {
111
+ return JSON.parse(stdout)
112
+ } catch (err) {
113
+ throw new Error(`Could not parse 1Password CLI output as JSON for item "${spec}".`)
114
+ }
115
+ }
116
+
117
+ module.exports = { runOp, readSecretRef, getItem }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "configorama-onepassword",
3
+ "version": "0.1.0",
4
+ "description": "1Password variable source plugin for configorama",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node parser.test.js && node normalize.test.js && node op-cli.test.js && node fields.test.js && node index.test.js && node smoke.test.js"
8
+ },
9
+ "keywords": [
10
+ "configorama",
11
+ "1password",
12
+ "secrets"
13
+ ],
14
+ "license": "MIT"
15
+ }
@@ -0,0 +1,70 @@
1
+ /* Parses 1Password field text as INI/dotenv for key path reads
2
+ Raw field text is only parsed when a config reference asks for a key path */
3
+ const ini = require('ini')
4
+
5
+ /**
6
+ * Restore ini module coercions so secret values stay strings.
7
+ * The ini parser coerces true/false/null which corrupts literal
8
+ * secret text like FLAG=true.
9
+ * @param {*} value - Parsed ini node
10
+ * @returns {*} Node with primitives restored to strings
11
+ */
12
+ function stringifyPrimitives(value) {
13
+ if (value === true) return 'true'
14
+ if (value === false) return 'false'
15
+ if (value === null) return 'null'
16
+ if (typeof value === 'object') {
17
+ const result = {}
18
+ for (const key of Object.keys(value)) {
19
+ result[key] = stringifyPrimitives(value[key])
20
+ }
21
+ return result
22
+ }
23
+ return value
24
+ }
25
+
26
+ /**
27
+ * Parse structured secret text.
28
+ * V1 supports INI/dotenv only. Future format detection order:
29
+ * 1. explicit format option on the ref
30
+ * 2. JSON if the trimmed value starts with { or [
31
+ * 3. YAML only with proof it is not confused with INI
32
+ * 4. INI fallback
33
+ * @param {string} value - Raw field text
34
+ * @param {object} [options] - Reserved for future format selection
35
+ * @returns {object} Parsed key/value structure
36
+ */
37
+ function parseStructuredSecret(value, options = {}) {
38
+ let parsed
39
+ try {
40
+ parsed = ini.parse(value)
41
+ } catch (err) {
42
+ throw new Error(`Could not parse 1Password field "${options.fieldName || 'value'}" as INI/dotenv.`)
43
+ }
44
+ return stringifyPrimitives(parsed)
45
+ }
46
+
47
+ /**
48
+ * Read a dot-separated key path from a parsed structure.
49
+ * Error messages never include secret values.
50
+ * @param {object} parsed - Result of parseStructuredSecret
51
+ * @param {string} keyPath - Dot-separated path, e.g. "database.password"
52
+ * @param {string} fieldName - 1Password field name for error messages
53
+ * @returns {string} Selected value
54
+ */
55
+ function getKeyPath(parsed, keyPath, fieldName) {
56
+ const segments = keyPath.split('.')
57
+ let current = parsed
58
+ for (const segment of segments) {
59
+ if (current === null || typeof current !== 'object' || !(segment in current)) {
60
+ throw new Error(`Key path "${keyPath}" was not found in 1Password field "${fieldName}".`)
61
+ }
62
+ current = current[segment]
63
+ }
64
+ if (typeof current === 'object') {
65
+ throw new Error(`Key path "${keyPath}" was not found in 1Password field "${fieldName}".`)
66
+ }
67
+ return current
68
+ }
69
+
70
+ module.exports = { parseStructuredSecret, getKeyPath }
@@ -0,0 +1,15 @@
1
+ /* Factory entry for configorama.sync() workers
2
+ Rebuilds the 1Password resolver from JSON-serializable options inside the sync worker */
3
+ const createOnePasswordResolver = require('./index')
4
+
5
+ /**
6
+ * @param {object} options - JSON-round-tripped syncOptions ({ refs, account, configDir, skipResolution })
7
+ * @returns {object} Variable source
8
+ */
9
+ module.exports = function createSyncOnePasswordResolver(options = {}) {
10
+ if (options.hasInjectedExecFile) {
11
+ throw new Error('1Password resolver options are not serializable for sync usage. Remove the injected execFile or use the async API.')
12
+ }
13
+ const { hasInjectedExecFile, ...factoryOptions } = options
14
+ return createOnePasswordResolver(factoryOptions)
15
+ }
package/src/errors.js CHANGED
@@ -16,6 +16,9 @@ const ERROR_CODES = [
16
16
  { code: 'no_input_file', description: 'No config file was provided on the command line.' },
17
17
  { code: 'file_not_found', description: 'The provided config file path does not exist.' },
18
18
  { code: 'path_not_found', description: 'A jq-style extraction path matched nothing in the resolved config.' },
19
+ { code: 'invalid_env_key', description: 'A setup write target received a key that is not a valid dotenv name.' },
20
+ { code: 'target_file_exists', description: 'A setup write target already exists and neither --merge nor --force was passed.' },
21
+ { code: 'managed_block_invalid', description: 'A dotenv merge target has missing, duplicated, or out-of-order managed block markers.' },
19
22
  { code: 'configorama_error', description: 'Generic, unclassified configorama error.' },
20
23
  ]
21
24
 
package/src/index.js CHANGED
@@ -4,6 +4,9 @@ const enrichMetadata = require('./utils/parsing/enrichMetadata')
4
4
  const { buildVariableSyntax } = require('./utils/variables/variableUtils')
5
5
  const { serializeRequirements } = require('./utils/requirements/serializeRequirements')
6
6
  const { buildConfigRequirements } = require('./utils/requirements/configRequirements')
7
+ const { runSetup } = require('./utils/setup/setupEngine')
8
+ const { writeDotenv } = require('./utils/setup/writeDotenv')
9
+ const { writeAnswers } = require('./utils/setup/writeAnswers')
7
10
  const { buildIntrospection } = require('./utils/introspection/model')
8
11
  const { buildAuditReport } = require('./utils/introspection/audit')
9
12
  const { formatGraph } = require('./utils/introspection/graph')
@@ -142,6 +145,22 @@ module.exports.analyze = async (configPathOrObject, settings = {}) => {
142
145
  return analysis
143
146
  }
144
147
 
148
+ /**
149
+ * Run the interactive setup wizard and return grouped answers without applying them
150
+ * @param {string|Object} configPathOrObject - Path to config file or raw config object
151
+ * @param {object} [settings] - Same settings as the main API, plus promptRenderer/streams
152
+ * @return {Promise<object>} { schemaVersion, configPath, requirements, answers, redactedAnswers }
153
+ */
154
+ module.exports.setup = async (configPathOrObject, settings = {}) => {
155
+ return runSetup(configPathOrObject, settings, { analyze: module.exports.analyze })
156
+ }
157
+
158
+ /**
159
+ * Safe writers for persisting setup answers (0600, atomic, overwrite-guarded)
160
+ */
161
+ module.exports.writeDotenv = writeDotenv
162
+ module.exports.writeAnswers = writeAnswers
163
+
145
164
  module.exports.introspect = async (configPathOrObject, settings = {}) => {
146
165
  const analysis = await module.exports.analyze(configPathOrObject, {
147
166
  ...settings,
@@ -163,7 +182,14 @@ module.exports.audit = async (configPathOrObject, settings = {}) => {
163
182
  const requirements = buildConfigRequirements(analysis)
164
183
  const introspection = buildIntrospection(analysis, { requirements })
165
184
  const customResolvers = Array.isArray(settings.variableSources)
166
- ? settings.variableSources.map(source => source.type).filter(Boolean)
185
+ ? settings.variableSources
186
+ .filter(source => source.type)
187
+ .map(source => ({
188
+ type: source.type,
189
+ sensitive: source.sensitive,
190
+ risk: source.risk,
191
+ description: source.description,
192
+ }))
167
193
  : []
168
194
  const originalConfig = analysis.originalConfig || {}
169
195
  return buildAuditReport(introspection, {
package/src/main.js CHANGED
@@ -92,6 +92,7 @@ const { mergeByKeys } = require('./utils/parsing/mergeByKeys')
92
92
  const { arrayToJsonPath } = require('./utils/parsing/arrayToJsonPath')
93
93
  /* Utils - paths */
94
94
  const { findLineByPath } = require('./utils/paths/findLineForKey')
95
+ const { configFileType } = require('./utils/paths/fileType')
95
96
  const { normalizeIgnorePaths, compileIgnorePaths, shouldIgnorePath } = require('./utils/paths/ignorePaths')
96
97
  /* Utils - regex */
97
98
  const { combineRegexes, funcRegex, fileRefSyntax, textRefSyntax } = require('./utils/regex')
@@ -109,9 +110,8 @@ const { validateOneOf } = require('./utils/filters/oneOf')
109
110
  const chalk = require('./utils/ui/chalk')
110
111
  const deepLog = require('./utils/ui/deep-log')
111
112
  const { logHeader } = require('./utils/ui/logs')
112
- const { runConfigWizard } = require('./utils/ui/configWizard')
113
- const { buildConfigRequirements } = require('./utils/requirements/configRequirements')
114
- const { redactUserInputsByRequirements } = require('./utils/redaction/setupRedaction')
113
+ const { runSetup } = require('./utils/setup/setupEngine')
114
+ const { applyAnswers } = require('./utils/setup/applyAnswers')
115
115
  /* Display */
116
116
  const { displayNoVariablesFound, displayVariableDetails, displayUniqueVariables, displayConfigurableVariables } = require('./display')
117
117
  /* Metadata */
@@ -175,7 +175,6 @@ const functionPrefixPattern = /^> function /
175
175
 
176
176
  let DEBUG = process.argv.includes('--debug') ? true : false
177
177
  let VERBOSE = process.argv.includes('--verbose') ? true : false
178
- let SETUP_MODE = process.argv.includes('--setup') ? true : false
179
178
  // DEBUG = true
180
179
  let DEBUG_TYPE = false
181
180
 
@@ -187,8 +186,8 @@ class Configorama {
187
186
  }
188
187
 
189
188
  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
189
+ // Setup wizard is explicit opt-in: the CLI translates --setup/`setup` into options.setup
190
+ this.setupMode = options.setup === true
192
191
  // Set opts to pass into JS file calls
193
192
  this.settings = Object.assign({}, {
194
193
  // Allow unknown ${xyz:...} syntax where xyz is not a registered resolver
@@ -939,34 +938,25 @@ class Configorama {
939
938
  if (this.setupMode) {
940
939
  logHeader('Setup Mode')
941
940
  // deepLog('enrich', enrich)
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)
941
+ const setupResult = await runSetup(this.configFilePath || this.config, this.settings, {
942
+ analysis: Object.assign({ originalConfig: this.originalConfig }, enrich),
943
+ })
944
+ this.setupRequirements = setupResult.requirements
945
+
946
+ // Summary shows only answered groups, sensitive values redacted
947
+ const displayInputs = {}
948
+ for (const [group, values] of Object.entries(setupResult.redactedAnswers)) {
949
+ if (Object.keys(values).length > 0) {
950
+ displayInputs[group] = values
951
+ }
952
+ }
946
953
 
947
954
  logHeader('User Inputs Summary')
948
955
  console.log()
949
956
  console.log(JSON.stringify(displayInputs, null, 2))
950
957
 
951
- // TODO set values
952
-
953
- // Apply user inputs to options and environment
954
- if (userInputs.options) {
955
- Object.assign(this.options, userInputs.options)
956
- }
957
- if (userInputs.env) {
958
- Object.assign(process.env, userInputs.env)
959
- }
960
-
961
- if (userInputs.self) {
962
- Object.assign(this.config, userInputs.self)
963
- }
964
-
965
- if (userInputs.dotProp) {
966
- for (const [key, value] of Object.entries(userInputs.dotProp)) {
967
- dotProp.set(this.config, key, value)
968
- }
969
- }
958
+ // Apply user inputs to options, environment, and config
959
+ applyAnswers({ options: this.options, env: process.env, config: this.config }, setupResult.answers)
970
960
 
971
961
  console.log()
972
962
  logHeader('Resolving Configuration')
@@ -2574,8 +2564,8 @@ Missing Value ${missingValue} - ${matchedString}
2574
2564
  if (valueCount.length === 1 && noNestedVars) {
2575
2565
  let lineInfo = ''
2576
2566
  if (this.originalString && this.configFilePath && valueObject.path) {
2577
- const ext = path.extname(this.configFilePath)
2578
- if (ext === '.yml' || ext === '.yaml' || ext === '.json') {
2567
+ const ext = configFileType(this.configFilePath)
2568
+ if (ext === '.yml' || ext === '.yaml' || ext === '.json' || ext === '.env') {
2579
2569
  const rawLines = this.originalString.split('\n')
2580
2570
  const lineNum = findLineByPath(arrayToJsonPath(valueObject.path), rawLines, ext)
2581
2571
  if (lineNum) lineInfo = ` at line ${lineNum},`
@@ -3049,7 +3039,9 @@ Missing Value ${missingValue} - ${matchedString}
3049
3039
  // ###############
3050
3040
  initialCall(func) {
3051
3041
  this.deep = []
3052
- this.tracker.start()
3042
+ // Progress reporting is a debug-only aid; the tracker still coordinates
3043
+ // promises and detects cycles regardless.
3044
+ this.tracker.start(DEBUG)
3053
3045
  return func().finally(() => {
3054
3046
  this.tracker.stop()
3055
3047
  this.deep = []
@@ -0,0 +1,13 @@
1
+ // Parses .env file contents into a flat key/value object
2
+ // Values keep their raw text so configorama can resolve ${...} references in them
3
+ const dotenv = require('dotenv')
4
+
5
+ /**
6
+ * @param {string} contents - Raw .env file text
7
+ * @returns {Object} Flat map of KEY -> raw string value
8
+ */
9
+ function parse(contents) {
10
+ return dotenv.parse(contents)
11
+ }
12
+
13
+ module.exports = { parse }
@@ -266,7 +266,8 @@ Default fallback value will be used if provided.
266
266
 
267
267
  ${JSON.stringify(options.context, null, 2)}`,
268
268
  })
269
- console.log(errorMsg)
269
+ // Diagnostic goes to stderr so it never pollutes resolved-config stdout
270
+ console.error(errorMsg)
270
271
  }
271
272
  // TODO maybe reject. YAML does not allow for null/undefined values
272
273
  // return Promise.reject(new Error(errorMsg))
package/src/sync.js CHANGED
@@ -9,6 +9,21 @@ const enrichMetadata = require('./utils/parsing/enrichMetadata')
9
9
  */
10
10
  module.exports = function configoramaSync(variableSources = []) {
11
11
  const customVariableSources = variableSources.map((varSrc) => {
12
+ /* Plugin factories: match/resolver do not survive the JSON trip through
13
+ sync-rpc, so plugins carry a syncFactory path + JSON syncOptions and
14
+ the real source is rebuilt here inside the worker process */
15
+ if (varSrc.syncFactory) {
16
+ const factoryPath = getFullPath(varSrc.syncFactory)
17
+ if (!fs.existsSync(factoryPath)) {
18
+ throw new Error(`Sync factory missing. Can't find ${factoryPath}`)
19
+ }
20
+ const createSource = require(factoryPath)
21
+ if (typeof createSource !== 'function') {
22
+ throw new Error(`Sync factory must export a function. Check ${factoryPath}`)
23
+ }
24
+ return createSource(varSrc.syncOptions || {})
25
+ }
26
+
12
27
  if (!varSrc.match || typeof varSrc.match !== 'string') {
13
28
  throw new Error('Variable source must be string for .sync usage')
14
29
  }
@@ -67,6 +82,18 @@ module.exports = function configoramaSync(variableSources = []) {
67
82
  instance.variableTypes
68
83
  )
69
84
 
85
+ /* Collect custom metadata from worker-side sources, mirroring the
86
+ collectMetadata loop the async API runs in src/index.js */
87
+ for (const source of customVariableSources) {
88
+ if (typeof source.collectMetadata === 'function') {
89
+ const customData = source.collectMetadata()
90
+ if (customData !== undefined && customData !== null) {
91
+ const metadataKey = source.metadataKey || `${source.type}References`
92
+ enrichedMetadata[metadataKey] = customData
93
+ }
94
+ }
95
+ }
96
+
70
97
  return {
71
98
  variableSyntax: instance.variableSyntax,
72
99
  variableTypes: instance.variableTypes,
@@ -13,15 +13,21 @@ class PromiseTracker {
13
13
  this.startTime = Date.now()
14
14
  this.cursor = 0
15
15
  }
16
- start() {
16
+ start(enableProgress = false) {
17
17
  this.reset()
18
- this.interval = setInterval(this.report.bind(this), 2500)
18
+ // The tracker's job is cycle detection and promise coordination; the
19
+ // periodic progress report is a debug-only aid, off unless requested.
20
+ if (enableProgress) {
21
+ this.interval = setInterval(this.report.bind(this), 2500)
22
+ }
19
23
  }
20
24
  report() {
21
25
  const delta = Date.now() - this.startTime
22
26
  const pending = this.getPending()
23
27
  const dots = dotDotDot(this.cursor++, 100, '...')
24
- console.log([
28
+ // Diagnostic progress goes to stderr so it never pollutes stdout (resolved
29
+ // config output, or `configx --export` lines consumed by eval).
30
+ console.error([
25
31
  `Fetching Async values${dots}`,
26
32
  ].concat(
27
33
  pending.map((promise) => `- ${promise.waitList}`)
@@ -40,7 +46,8 @@ class PromiseTracker {
40
46
  /**/
41
47
  }
42
48
  stop() {
43
- clearInterval(this.interval)
49
+ if (this.interval) clearInterval(this.interval)
50
+ this.interval = null
44
51
  this.reset()
45
52
  }
46
53
  add(variable, promise, specifier, hasFilter, promiseKey) {