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.
- package/README.md +41 -1
- package/cli.js +14 -1
- package/package.json +18 -6
- package/plugins/cloudformation/README.md +79 -0
- package/plugins/cloudformation/credentials.js +231 -0
- package/plugins/cloudformation/index.js +204 -0
- package/plugins/cloudformation/package-lock.json +657 -0
- package/plugins/cloudformation/package.json +24 -0
- package/plugins/onepassword/README.md +175 -0
- package/plugins/onepassword/fields.js +126 -0
- package/plugins/onepassword/index.js +356 -0
- package/plugins/onepassword/normalize.js +157 -0
- package/plugins/onepassword/op-cli.js +117 -0
- package/plugins/onepassword/package.json +15 -0
- package/plugins/onepassword/parser.js +70 -0
- package/plugins/onepassword/sync-factory.js +15 -0
- package/src/errors.js +3 -0
- package/src/index.js +27 -1
- package/src/main.js +24 -32
- package/src/parsers/dotenv.js +13 -0
- package/src/resolvers/valueFromFile.js +2 -1
- package/src/sync.js +27 -0
- package/src/utils/PromiseTracker.js +11 -4
- package/src/utils/PromiseTracker.test.js +48 -0
- package/src/utils/introspection/audit.js +39 -9
- package/src/utils/parsing/detectFormat.js +70 -0
- package/src/utils/parsing/detectFormat.test.js +68 -0
- package/src/utils/parsing/parse.js +11 -37
- package/src/utils/paths/fileType.js +27 -0
- package/src/utils/paths/fileType.test.js +41 -0
- package/src/utils/paths/findLineForKey.js +24 -1
- package/src/utils/paths/findLineForKey.test.js +20 -1
- package/src/utils/setup/applyAnswers.js +41 -0
- package/src/utils/setup/setupEngine.js +85 -0
- package/src/utils/setup/stdoutRedirect.js +48 -0
- package/src/utils/setup/writeAnswers.js +50 -0
- package/src/utils/setup/writeDotenv.js +142 -0
- package/src/utils/ui/configWizard.js +20 -1
- package/types/src/capabilities.d.ts +8 -0
- package/types/src/capabilities.d.ts.map +1 -0
- package/types/src/display.d.ts +62 -0
- package/types/src/display.d.ts.map +1 -0
- package/types/src/errors.d.ts +20 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/index.d.ts +119 -0
- package/types/src/index.d.ts.map +1 -1
- package/types/src/main.d.ts +23 -16
- package/types/src/main.d.ts.map +1 -1
- package/types/src/metadata.d.ts +28 -0
- package/types/src/metadata.d.ts.map +1 -0
- package/types/src/parsers/dotenv.d.ts +6 -0
- package/types/src/parsers/dotenv.d.ts.map +1 -0
- package/types/src/parsers/esm.d.ts.map +1 -1
- package/types/src/parsers/typescript.d.ts.map +1 -1
- package/types/src/resolvers/valueFromCron.d.ts.map +1 -1
- package/types/src/resolvers/valueFromEval.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts +4 -0
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
- package/types/src/resolvers/valueFromOptions.d.ts +1 -0
- package/types/src/resolvers/valueFromOptions.d.ts.map +1 -1
- package/types/src/sync.d.ts.map +1 -1
- package/types/src/utils/BoundedMap.d.ts +10 -0
- package/types/src/utils/BoundedMap.d.ts.map +1 -0
- package/types/src/utils/PromiseTracker.d.ts +1 -1
- package/types/src/utils/PromiseTracker.d.ts.map +1 -1
- package/types/src/utils/filters/filterArgs.d.ts +13 -0
- package/types/src/utils/filters/filterArgs.d.ts.map +1 -0
- package/types/src/utils/filters/oneOf.d.ts +7 -0
- package/types/src/utils/filters/oneOf.d.ts.map +1 -0
- package/types/src/utils/introspection/audit.d.ts +14 -0
- package/types/src/utils/introspection/audit.d.ts.map +1 -0
- package/types/src/utils/introspection/graph.d.ts +4 -0
- package/types/src/utils/introspection/graph.d.ts.map +1 -0
- package/types/src/utils/introspection/model.d.ts +43 -0
- package/types/src/utils/introspection/model.d.ts.map +1 -0
- package/types/src/utils/parsing/commentAnnotations.d.ts +18 -0
- package/types/src/utils/parsing/commentAnnotations.d.ts.map +1 -0
- package/types/src/utils/parsing/detectFormat.d.ts +17 -0
- package/types/src/utils/parsing/detectFormat.d.ts.map +1 -0
- package/types/src/utils/parsing/enrichMetadata.d.ts.map +1 -1
- package/types/src/utils/parsing/extractComment.d.ts +20 -0
- package/types/src/utils/parsing/extractComment.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/fileType.d.ts +15 -0
- package/types/src/utils/paths/fileType.d.ts.map +1 -0
- package/types/src/utils/paths/findLineForKey.d.ts +9 -0
- package/types/src/utils/paths/findLineForKey.d.ts.map +1 -1
- package/types/src/utils/paths/ignorePaths.d.ts +5 -0
- package/types/src/utils/paths/ignorePaths.d.ts.map +1 -0
- package/types/src/utils/redaction/redact.d.ts +10 -0
- package/types/src/utils/redaction/redact.d.ts.map +1 -0
- package/types/src/utils/redaction/setupRedaction.d.ts +5 -0
- package/types/src/utils/redaction/setupRedaction.d.ts.map +1 -0
- package/types/src/utils/requirements/configRequirements.d.ts +70 -0
- package/types/src/utils/requirements/configRequirements.d.ts.map +1 -0
- package/types/src/utils/requirements/serializeRequirements.d.ts +63 -0
- package/types/src/utils/requirements/serializeRequirements.d.ts.map +1 -0
- package/types/src/utils/security/dotenvFileRefs.d.ts +13 -0
- package/types/src/utils/security/dotenvFileRefs.d.ts.map +1 -0
- package/types/src/utils/security/evalSafety.d.ts +17 -0
- package/types/src/utils/security/evalSafety.d.ts.map +1 -0
- package/types/src/utils/security/safetyPolicy.d.ts +15 -0
- package/types/src/utils/security/safetyPolicy.d.ts.map +1 -0
- package/types/src/utils/setup/applyAnswers.d.ts +34 -0
- package/types/src/utils/setup/applyAnswers.d.ts.map +1 -0
- package/types/src/utils/setup/setupEngine.d.ts +89 -0
- package/types/src/utils/setup/setupEngine.d.ts.map +1 -0
- package/types/src/utils/setup/stdoutRedirect.d.ts +10 -0
- package/types/src/utils/setup/stdoutRedirect.d.ts.map +1 -0
- package/types/src/utils/setup/writeAnswers.d.ts +17 -0
- package/types/src/utils/setup/writeAnswers.d.ts.map +1 -0
- package/types/src/utils/setup/writeDotenv.d.ts +27 -0
- package/types/src/utils/setup/writeDotenv.d.ts.map +1 -0
- package/types/src/utils/strings/didYouMean.d.ts +23 -0
- package/types/src/utils/strings/didYouMean.d.ts.map +1 -0
- package/types/src/utils/strings/formatFunctionArgs.d.ts.map +1 -1
- package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
- package/types/src/utils/strings/splitByComma.d.ts.map +1 -1
- package/types/src/utils/ui/configWizard.d.ts +51 -7
- package/types/src/utils/ui/configWizard.d.ts.map +1 -1
- package/types/src/utils/ui/createEditorLink.d.ts +7 -0
- package/types/src/utils/ui/createEditorLink.d.ts.map +1 -1
- package/types/src/utils/ui/promptDescriptors.d.ts +39 -0
- package/types/src/utils/ui/promptDescriptors.d.ts.map +1 -0
- package/types/src/utils/variables/cleanVariable.d.ts.map +1 -1
- package/types/src/utils/variables/getVariableType.d.ts.map +1 -1
- 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
|
|
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 {
|
|
113
|
-
const {
|
|
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
|
|
191
|
-
this.setupMode =
|
|
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
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|