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,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "configorama-cloudformation",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "CloudFormation variable source plugin for configorama",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node index.test.js",
|
|
8
|
+
"publish": "git push origin && git push origin --tags",
|
|
9
|
+
"release:patch": "npm version patch && npm publish",
|
|
10
|
+
"release:minor": "npm version minor && npm publish",
|
|
11
|
+
"release:major": "npm version major && npm publish"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"configorama",
|
|
15
|
+
"cloudformation",
|
|
16
|
+
"aws",
|
|
17
|
+
"serverless"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@aws-sdk/client-cloudformation": "^3.700.0",
|
|
22
|
+
"@aws-sdk/credential-providers": "^3.700.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# configorama 1Password plugin
|
|
2
|
+
|
|
3
|
+
Resolves configuration values from 1Password through the [`op` CLI](https://developer.1password.com/docs/cli/).
|
|
4
|
+
|
|
5
|
+
Secrets are fetched at resolution time. The plugin never persists them, never logs CLI output, and never passes resolved values as command-line arguments. Resolved values do enter the config object returned to your code — treat that object as sensitive.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Install the [1Password CLI](https://developer.1password.com/docs/cli/get-started/) and make sure `op` is on `PATH`.
|
|
10
|
+
- Sign in with `op signin`, enable the 1Password app integration, or set `OP_SERVICE_ACCOUNT_TOKEN`. The CLI reads `OP_ACCOUNT`, `OP_CONFIG_DIR`, and `OP_SERVICE_ACCOUNT_TOKEN` on its own — the plugin does not manage auth.
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
const configorama = require('configorama')
|
|
16
|
+
const createOnePasswordResolver = require('configorama/plugins/onepassword')
|
|
17
|
+
|
|
18
|
+
const opResolver = createOnePasswordResolver({
|
|
19
|
+
refs: {
|
|
20
|
+
npm: 'op://production/npm-automation/notesPlain',
|
|
21
|
+
database: {
|
|
22
|
+
item: 'database-prod',
|
|
23
|
+
vault: 'production',
|
|
24
|
+
field: 'password'
|
|
25
|
+
},
|
|
26
|
+
github: {
|
|
27
|
+
item: 'GitHub',
|
|
28
|
+
vault: 'development',
|
|
29
|
+
section: 'credentials',
|
|
30
|
+
field: 'personal_token'
|
|
31
|
+
},
|
|
32
|
+
copiedLink: 'https://start.1password.com/open/i?a=ACCT&v=VAULT_ID&i=ITEM_ID&h=my.1password.com'
|
|
33
|
+
},
|
|
34
|
+
// account: 'my', // passed to op as --account
|
|
35
|
+
// configDir: '/op/config', // passed to op as --config
|
|
36
|
+
// skipResolution: true, // collect metadata, return placeholders, never call op
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const config = await configorama('config.yml', {
|
|
40
|
+
variableSources: [opResolver]
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
|
|
46
|
+
| Option | Type | Description |
|
|
47
|
+
|--------|------|-------------|
|
|
48
|
+
| `refs` | object | Alias map. Values may be an `op://` secret reference, an item ID/name, a private item link, or `{ item, vault, section, field }` / `{ ref }` / `{ url }` |
|
|
49
|
+
| `account` | string | Passed to `op` as `--account` |
|
|
50
|
+
| `configDir` | string | Passed to `op` as `--config` |
|
|
51
|
+
| `opPath` | string | Path to the `op` binary (defaults to `op` on `PATH`) |
|
|
52
|
+
| `skipResolution` | boolean | Record metadata and return deterministic placeholders without calling `op` |
|
|
53
|
+
|
|
54
|
+
## Alias syntax (recommended)
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
npmToken: ${op:npm.NPM_TOKEN}
|
|
58
|
+
dbPassword: ${op:database}
|
|
59
|
+
rawNote: ${op:npm}
|
|
60
|
+
nested: ${op:npm.database.password}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Alias names may contain only letters, numbers, and underscores. Dots separate the alias from a key path.
|
|
64
|
+
|
|
65
|
+
## Direct function syntax
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
directItem: ${op(item-id-example).NPM_TOKEN}
|
|
69
|
+
spacedName: ${op(My Database Login).password}
|
|
70
|
+
directRef: ${op(op://vault/item/field)}
|
|
71
|
+
directRefKey: ${op(op://vault/item/notesPlain).NPM_TOKEN}
|
|
72
|
+
privateLink: ${op(https://start.1password.com/open/i?a=ACCT&v=VAULT_ID&i=ITEM_ID&h=my.1password.com).NPM_TOKEN}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The parentheses give the parser a reliable boundary for specs containing dots, slashes, spaces, or query parameters. Prefer aliases for readability and auditability.
|
|
76
|
+
|
|
77
|
+
### Unsupported syntax
|
|
78
|
+
|
|
79
|
+
- `${op:op://vault/item/field}` — colon syntax is reserved for aliases. Error message points you to `${op(op://vault/item/field)}`.
|
|
80
|
+
- Private links in colon syntax.
|
|
81
|
+
- Public "share with anyone" links (`share.1password.com`) in any position. Use **Copy Private Link** or an `op://` secret reference instead.
|
|
82
|
+
|
|
83
|
+
## Private item links
|
|
84
|
+
|
|
85
|
+
Links copied with **Copy Private Link** are supported:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
https://start.1password.com/open/i?a=ACCOUNT&v=VAULT_ID&i=ITEM_ID&h=my.1password.com
|
|
89
|
+
onepassword://open/i?a=ACCOUNT&v=VAULT_ID&i=ITEM_ID&h=my.1password.com
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The plugin extracts `i` (item ID) and `v` (vault ID) and fetches with `op item get <itemId> --vault <vaultId>`. The `a`/`h` parameters and the raw URL are never stored in metadata. A link without `v` still resolves, with a warning diagnostic — service accounts and duplicate item names may require vault scoping.
|
|
93
|
+
|
|
94
|
+
## Field selection
|
|
95
|
+
|
|
96
|
+
With an explicit `field` on a ref, the plugin matches against the 1Password field `id`, `label`, and `purpose` (case-insensitive). `section` matches `section.id`/`section.label` and is required when the same label exists in multiple sections.
|
|
97
|
+
|
|
98
|
+
Without a configured `field`, the plugin infers a single secret field:
|
|
99
|
+
|
|
100
|
+
- `notesPlain`/notes, `purpose: PASSWORD`, `type: CONCEALED`, or secret-like labels (`password`, `token`, `api_key`, `secret`, `credential`, `private key`) are candidates
|
|
101
|
+
- username, email, URL, and other metadata fields are ignored
|
|
102
|
+
- exactly one candidate wins; zero or multiple candidates throw an error telling you to set `field` (and `section`)
|
|
103
|
+
|
|
104
|
+
Ambiguity is always an error — the plugin never silently prefers `notesPlain` over `password`.
|
|
105
|
+
|
|
106
|
+
In direct/alias syntax without a configured field, the first key path segment may select a field by name: `${op(My Login).password}` picks the `password` field. If no field matches, the segment is treated as an INI key inside the inferred field: `${op(note-item).NPM_TOKEN}`.
|
|
107
|
+
|
|
108
|
+
## Structured notes (INI/dotenv)
|
|
109
|
+
|
|
110
|
+
A secure note like:
|
|
111
|
+
|
|
112
|
+
```ini
|
|
113
|
+
# npm automation token
|
|
114
|
+
NPM_TOKEN=npm_xxx
|
|
115
|
+
|
|
116
|
+
[database]
|
|
117
|
+
password=s3cr3t
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
resolves as:
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
npmToken: ${op:npm.NPM_TOKEN} # npm_xxx
|
|
124
|
+
dbPassword: ${op:npm.database.password} # s3cr3t
|
|
125
|
+
rawNote: ${op:npm} # the whole note text, unparsed
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`${op:alias}` always returns the raw field text — even when it contains `=`. Parsing only happens when a key path is requested, so tokens, base64, and connection strings are never corrupted.
|
|
129
|
+
|
|
130
|
+
## skipResolution
|
|
131
|
+
|
|
132
|
+
With `skipResolution: true` the resolver records metadata and returns deterministic placeholders without calling `op`:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
[OP:alias:npm.NPM_TOKEN]
|
|
136
|
+
[OP:secretRef:op://vault/item/field]
|
|
137
|
+
[OP:item:item-id:NPM_TOKEN]
|
|
138
|
+
[OP:privateLink:item-id]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Sync usage
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
const config = configorama.sync('config.yml', {
|
|
145
|
+
variableSources: [createOnePasswordResolver({ refs })]
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Sync mode rebuilds the resolver inside a worker process from JSON-serializable options and always uses the real `op` binary.
|
|
150
|
+
|
|
151
|
+
## Auth prompts
|
|
152
|
+
|
|
153
|
+
1Password app-integration auth is per `op` process. The resolver runs the first `op` call alone and queues the rest behind it, so a config referencing many items triggers at most one biometric/authorization prompt per resolution run instead of one per item.
|
|
154
|
+
|
|
155
|
+
The 1Password dialog names the terminal app (OS-attributed, not customizable), so before the first call the resolver prints a context line to stderr in interactive terminals:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
configorama: requesting 3 items from 1Password (expect an authorization prompt)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The hint is TTY-only — silent in CI and pipes.
|
|
162
|
+
|
|
163
|
+
## Security model
|
|
164
|
+
|
|
165
|
+
- Secrets are fetched from `op` at resolution time. The plugin never writes them to disk, never logs CLI output, and never passes resolved values as command-line arguments. Resolved values **do** enter the config object returned to your code — treat that object as sensitive (don't log it, commit it, or emit it to an untrusted sink).
|
|
166
|
+
- **Direct syntax reads whatever your `op` session can reach.** `${op(op://any-vault/any-item/field)}`, direct item IDs, and private links are not restricted to the aliases in `refs`. If you resolve a config file you do not fully trust while a 1Password session is unlocked, that config can read any secret the session can access. For untrusted config, run with `safeMode: true` (which blocks all custom resolvers) or audit first — `configorama.audit()` flags this resolver as high-severity `remote_secret_read`.
|
|
167
|
+
|
|
168
|
+
## Safe mode and audit
|
|
169
|
+
|
|
170
|
+
- `safeMode: true` blocks all custom resolvers during resolution, including this one.
|
|
171
|
+
- `configorama.audit()` registers the plugin without fetching secrets and reports a high-severity `remote_secret_read` finding, because this resolver returns secret material.
|
|
172
|
+
|
|
173
|
+
## Metadata
|
|
174
|
+
|
|
175
|
+
With `returnMetadata: true`, references are collected under `metadata.opReferences` — reference kinds, item/vault IDs, fields, key paths, and config paths. Secret values, full private links, and CLI output are never recorded.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/* Selects a field from op item get JSON, explicitly or by inference
|
|
2
|
+
Ambiguity is always an error - never silently prefers one secret field over another */
|
|
3
|
+
|
|
4
|
+
const SECRET_LABELS = new Set(['password', 'token', 'api_key', 'api key', 'secret', 'credential', 'private key'])
|
|
5
|
+
const IGNORED_PURPOSES = new Set(['USERNAME'])
|
|
6
|
+
const IGNORED_TYPES = new Set(['URL'])
|
|
7
|
+
const IGNORED_LABELS = new Set(['username', 'email', 'url', 'website', 'created', 'updated', 'title'])
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} item - Parsed op item get JSON
|
|
11
|
+
* @returns {string} Display name for error messages
|
|
12
|
+
*/
|
|
13
|
+
function itemName(item) {
|
|
14
|
+
return item.title || item.id || 'unknown'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string|undefined} value - Field attribute
|
|
19
|
+
* @param {string} wanted - Configured name
|
|
20
|
+
* @returns {boolean} Case-insensitive equality
|
|
21
|
+
*/
|
|
22
|
+
function matches(value, wanted) {
|
|
23
|
+
return typeof value === 'string' && value.toLowerCase() === wanted.toLowerCase()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Select a field from an item.
|
|
28
|
+
* With options.field: explicit matching on id/label/purpose (+ section).
|
|
29
|
+
* Without: inference over secret-content candidates; ambiguity throws.
|
|
30
|
+
* @param {object} item - Parsed op item get JSON
|
|
31
|
+
* @param {object} [options] - { field, section }
|
|
32
|
+
* @returns {object} The selected field object
|
|
33
|
+
*/
|
|
34
|
+
function selectField(item, options = {}) {
|
|
35
|
+
const fields = item.fields || []
|
|
36
|
+
if (options.field) {
|
|
37
|
+
return selectExplicit(item, fields, options.field, options.section)
|
|
38
|
+
}
|
|
39
|
+
return inferField(item, fields)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {object} item - Item for error messages
|
|
44
|
+
* @param {object[]} fields - item.fields
|
|
45
|
+
* @param {string} wanted - Configured field name
|
|
46
|
+
* @param {string} [section] - Configured section name
|
|
47
|
+
* @returns {object} Matched field
|
|
48
|
+
*/
|
|
49
|
+
function selectExplicit(item, fields, wanted, section) {
|
|
50
|
+
const found = trySelectField(item, { field: wanted, section })
|
|
51
|
+
if (!found) {
|
|
52
|
+
throw new Error(`Field "${wanted}" was not found in 1Password item "${itemName(item)}".`)
|
|
53
|
+
}
|
|
54
|
+
return found
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Match a field by name, returning null when nothing matches.
|
|
59
|
+
* Still throws on ambiguity - duplicate labels always need a section.
|
|
60
|
+
* @param {object} item - Parsed op item get JSON
|
|
61
|
+
* @param {object} options - { field, section }
|
|
62
|
+
* @returns {object|null} Matched field or null
|
|
63
|
+
*/
|
|
64
|
+
function trySelectField(item, options) {
|
|
65
|
+
const fields = item.fields || []
|
|
66
|
+
const wanted = options.field
|
|
67
|
+
let candidates = fields.filter((field) => {
|
|
68
|
+
return matches(field.id, wanted) || matches(field.label, wanted) || matches(field.purpose, wanted)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (options.section) {
|
|
72
|
+
candidates = candidates.filter((field) => {
|
|
73
|
+
const fieldSection = field.section || {}
|
|
74
|
+
return matches(fieldSection.id, options.section) || matches(fieldSection.label, options.section)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (candidates.length === 0) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
if (candidates.length > 1) {
|
|
82
|
+
throw new Error(`1Password item "${itemName(item)}" has multiple fields labeled "${wanted}". Set section explicitly.`)
|
|
83
|
+
}
|
|
84
|
+
return candidates[0]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {object} field - Item field
|
|
89
|
+
* @returns {boolean} True when the field likely holds secret content
|
|
90
|
+
*/
|
|
91
|
+
function isSecretCandidate(field) {
|
|
92
|
+
if (field.value === undefined || field.value === '') return false
|
|
93
|
+
const label = (field.label || '').toLowerCase()
|
|
94
|
+
const purpose = (field.purpose || '').toUpperCase()
|
|
95
|
+
const type = (field.type || '').toUpperCase()
|
|
96
|
+
|
|
97
|
+
if (IGNORED_PURPOSES.has(purpose) || IGNORED_TYPES.has(type) || IGNORED_LABELS.has(label)) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
if (matches(field.id, 'notesPlain') || matches(field.label, 'notesPlain') || purpose === 'NOTES') return true
|
|
101
|
+
if (purpose === 'PASSWORD') return true
|
|
102
|
+
if (type === 'CONCEALED') return true
|
|
103
|
+
if (SECRET_LABELS.has(label)) return true
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Infer the single secret-content field or throw.
|
|
109
|
+
* Never prefers notesPlain over password (or vice versa) - two candidates is an error.
|
|
110
|
+
* @param {object} item - Item for error messages
|
|
111
|
+
* @param {object[]} fields - item.fields
|
|
112
|
+
* @returns {object} Inferred field
|
|
113
|
+
*/
|
|
114
|
+
function inferField(item, fields) {
|
|
115
|
+
const candidates = fields.filter(isSecretCandidate)
|
|
116
|
+
if (candidates.length === 1) {
|
|
117
|
+
return candidates[0]
|
|
118
|
+
}
|
|
119
|
+
if (candidates.length > 1) {
|
|
120
|
+
const labels = candidates.map((field) => field.label || field.id).join(', ')
|
|
121
|
+
throw new Error(`1Password item "${itemName(item)}" has multiple candidate secret fields: ${labels}. Set field explicitly.`)
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`1Password item "${itemName(item)}" has no obvious secret field. Set field explicitly.`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { selectField, trySelectField }
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/* 1Password variable source for configorama
|
|
2
|
+
Resolves ${op:alias} and ${op(item)} references through the op CLI */
|
|
3
|
+
const { readSecretRef, getItem } = require('./op-cli')
|
|
4
|
+
const { validateAliasName, normalizeRefValue, isItemId } = require('./normalize')
|
|
5
|
+
const { selectField, trySelectField } = require('./fields')
|
|
6
|
+
const { parseStructuredSecret, getKeyPath } = require('./parser')
|
|
7
|
+
|
|
8
|
+
const OP_PREFIX = 'op'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tell interactive users why an authorization prompt is about to appear,
|
|
12
|
+
* naming what is being fetched. The 1Password dialog only names the terminal
|
|
13
|
+
* app (OS-attributed), so this line supplies the configorama context. Labels
|
|
14
|
+
* are config keys / aliases / fields — references, never secret values.
|
|
15
|
+
* TTY-only: silent in CI and pipes.
|
|
16
|
+
* @param {string[]} labels - Human labels for the values being fetched
|
|
17
|
+
* @param {string} programName - Host tool name shown as the message prefix
|
|
18
|
+
*/
|
|
19
|
+
function logAuthHint(labels, programName) {
|
|
20
|
+
if (!process.stderr.isTTY) return
|
|
21
|
+
const unique = [...new Set(labels)]
|
|
22
|
+
const n = unique.length
|
|
23
|
+
const what = n > 0 && n <= 8 ? unique.join(', ') : `${n} value${n === 1 ? '' : 's'}`
|
|
24
|
+
process.stderr.write(`${programName}: fetching ${what} from 1Password (expect an authorization prompt)\n`)
|
|
25
|
+
}
|
|
26
|
+
// Supports: op:alias, op:alias.KEY, op(item), op(item).KEY
|
|
27
|
+
const opVariableSyntax = /^op(?::|\()/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a 1Password variable source resolver.
|
|
31
|
+
*
|
|
32
|
+
* Syntax:
|
|
33
|
+
* ${op:alias} — alias from refs, raw selected field
|
|
34
|
+
* ${op:alias.KEY} — alias, INI/dotenv key path into the field
|
|
35
|
+
* ${op(spec)} — direct item ID/name, op:// ref, or private link
|
|
36
|
+
* ${op(spec).KEY} — direct spec with key path
|
|
37
|
+
*
|
|
38
|
+
* Colon syntax is reserved for aliases; raw op:// refs and links must use
|
|
39
|
+
* function syntax so the parser has a reliable spec boundary.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} options - Configuration options
|
|
42
|
+
* @param {object} [options.refs] - Alias map: name -> string ref, item name, private link, or { item, vault, section, field } / { ref } / { url }
|
|
43
|
+
* @param {string} [options.account] - Passed to op as --account
|
|
44
|
+
* @param {string} [options.configDir] - Passed to op as --config
|
|
45
|
+
* @param {string} [options.opPath] - Path to the op binary (defaults to "op" on PATH)
|
|
46
|
+
* @param {string} [options.programName] - Host tool name for the auth-prompt hint (defaults to $CONFIGORAMA_PROGRAM_NAME or "configorama")
|
|
47
|
+
* @param {boolean} [options.skipResolution] - Collect metadata and return placeholders without calling op
|
|
48
|
+
* @param {Function} [options.execFile] - execFile injection for tests (not serializable; unavailable in sync mode)
|
|
49
|
+
* @returns {object} Variable source configuration with resolver and metadata collector
|
|
50
|
+
*/
|
|
51
|
+
function createOnePasswordResolver(options = {}) {
|
|
52
|
+
const {
|
|
53
|
+
refs = {},
|
|
54
|
+
account,
|
|
55
|
+
configDir,
|
|
56
|
+
opPath,
|
|
57
|
+
programName,
|
|
58
|
+
skipResolution = false,
|
|
59
|
+
execFile,
|
|
60
|
+
} = options
|
|
61
|
+
|
|
62
|
+
const aliasRefs = {}
|
|
63
|
+
for (const alias of Object.keys(refs)) {
|
|
64
|
+
validateAliasName(alias)
|
|
65
|
+
aliasRefs[alias] = normalizeRefValue(refs[alias])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const opReferences = []
|
|
69
|
+
// Caches hold in-flight promises so concurrent references to the same
|
|
70
|
+
// item/ref share one op call. Failed promises are evicted immediately,
|
|
71
|
+
// so auth/not-found/parse failures are never cached.
|
|
72
|
+
const secretRefCache = new Map()
|
|
73
|
+
const itemCache = new Map()
|
|
74
|
+
|
|
75
|
+
const cliOptions = { account, configDir, opPath, execFile }
|
|
76
|
+
const scopeKey = `${account || ''}|${configDir || ''}`
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {Map} cache - Promise cache
|
|
80
|
+
* @param {string} key - Cache key
|
|
81
|
+
* @param {Function} fn - Producer returning a promise
|
|
82
|
+
* @param {string} [label] - Human label for the auth hint
|
|
83
|
+
* @returns {Promise<*>} Shared promise
|
|
84
|
+
*/
|
|
85
|
+
function cached(cache, key, fn, label) {
|
|
86
|
+
if (cache.has(key)) {
|
|
87
|
+
return cache.get(key)
|
|
88
|
+
}
|
|
89
|
+
const promise = withColdStartLatch(fn, label).catch((err) => {
|
|
90
|
+
cache.delete(key)
|
|
91
|
+
throw err
|
|
92
|
+
})
|
|
93
|
+
cache.set(key, promise)
|
|
94
|
+
return promise
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 1Password app-integration auth prompts once per op PROCESS until a
|
|
98
|
+
// session exists. Parallel resolution of N distinct items would spawn N
|
|
99
|
+
// processes at cold start and trigger N biometric prompts, so the first
|
|
100
|
+
// op call runs alone; everything else queues behind its settlement and
|
|
101
|
+
// then fans out in parallel against the authorized session.
|
|
102
|
+
let coldStartCall = null
|
|
103
|
+
let hintScheduled = false
|
|
104
|
+
const pendingLabels = []
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {Function} fn - Producer returning a promise
|
|
108
|
+
* @param {string} [label] - Human label for the auth hint
|
|
109
|
+
* @returns {Promise<*>} Producer result, gated behind the first call
|
|
110
|
+
*/
|
|
111
|
+
function withColdStartLatch(fn, label) {
|
|
112
|
+
if (label) pendingLabels.push(label)
|
|
113
|
+
if (!hintScheduled) {
|
|
114
|
+
hintScheduled = true
|
|
115
|
+
// setImmediate lets the whole parallel fan-out register first so the
|
|
116
|
+
// hint names every value being fetched. The prefix is read here (not at
|
|
117
|
+
// factory time) so a host tool can set CONFIGORAMA_PROGRAM_NAME after the
|
|
118
|
+
// resolver is constructed but before resolution runs.
|
|
119
|
+
setImmediate(() => {
|
|
120
|
+
const prefix = programName || process.env.CONFIGORAMA_PROGRAM_NAME || 'configorama'
|
|
121
|
+
logAuthHint(pendingLabels, prefix)
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
if (!coldStartCall) {
|
|
125
|
+
coldStartCall = fn()
|
|
126
|
+
return coldStartCall
|
|
127
|
+
}
|
|
128
|
+
return coldStartCall.catch(() => {}).then(fn)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Parse an op variable string into its reference and key path.
|
|
133
|
+
* @param {string} varString - e.g. "op:npm.NPM_TOKEN" or "op(item-id).KEY"
|
|
134
|
+
* @returns {{reference: object, keyPath: string|undefined, alias: string|undefined}} Parsed parts
|
|
135
|
+
*/
|
|
136
|
+
function parseVariable(varString) {
|
|
137
|
+
const trimmed = varString.trim()
|
|
138
|
+
|
|
139
|
+
const funcMatch = trimmed.match(/^op\((.*)\)(?:\.(.+))?$/)
|
|
140
|
+
if (funcMatch) {
|
|
141
|
+
const spec = funcMatch[1].trim()
|
|
142
|
+
if (!spec) {
|
|
143
|
+
throw new Error(`Invalid 1Password reference "${varString}". Expected \${op(item)}, \${op(op://vault/item/field)}, or a private link.`)
|
|
144
|
+
}
|
|
145
|
+
return { reference: normalizeRefValue(spec), keyPath: funcMatch[2], alias: undefined }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Bare 1Password secret reference: ${op://vault/item/field}. This is the
|
|
149
|
+
// native op:// URI and is treated as a direct secret ref (op read). Key
|
|
150
|
+
// paths are not supported here because op:// refs contain dots and slashes;
|
|
151
|
+
// use alias or function syntax for a structured-note key path.
|
|
152
|
+
if (trimmed.startsWith('op://')) {
|
|
153
|
+
return { reference: { kind: 'secretRef', ref: trimmed }, keyPath: undefined, alias: undefined }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!trimmed.startsWith('op:')) {
|
|
157
|
+
throw new Error(`Invalid 1Password variable "${varString}".`)
|
|
158
|
+
}
|
|
159
|
+
const rest = trimmed.slice(3)
|
|
160
|
+
if (rest.startsWith('op://')) {
|
|
161
|
+
throw new Error(`Use \${op(${rest})} for direct secret references.`)
|
|
162
|
+
}
|
|
163
|
+
// A private link (or any URL) after the colon is a common mistake — colon
|
|
164
|
+
// syntax is alias-only. Point to function syntax without echoing the link
|
|
165
|
+
// (it carries account/host params we treat as sensitive).
|
|
166
|
+
if (/^https?:\/\//.test(rest) || rest.startsWith('onepassword://')) {
|
|
167
|
+
throw new Error('1Password private links are not supported in colon syntax. Use function syntax: ${op(<private link>)}, or an op:// secret reference.')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const dotIndex = rest.indexOf('.')
|
|
171
|
+
const alias = dotIndex === -1 ? rest : rest.slice(0, dotIndex)
|
|
172
|
+
const keyPath = dotIndex === -1 ? undefined : rest.slice(dotIndex + 1)
|
|
173
|
+
|
|
174
|
+
validateAliasName(alias)
|
|
175
|
+
const reference = aliasRefs[alias]
|
|
176
|
+
if (reference) {
|
|
177
|
+
return { reference, keyPath, alias }
|
|
178
|
+
}
|
|
179
|
+
// Not a configured alias. A bare 1Password item ID (26-char base32) is
|
|
180
|
+
// unambiguous, so accept it as a direct item reference. Configured aliases
|
|
181
|
+
// always take precedence (checked above), so this only affects strings that
|
|
182
|
+
// would otherwise error as an unknown alias.
|
|
183
|
+
if (isItemId(alias)) {
|
|
184
|
+
return {
|
|
185
|
+
reference: { kind: 'item', item: alias, vault: undefined, section: undefined, field: undefined },
|
|
186
|
+
keyPath,
|
|
187
|
+
alias: undefined,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`Unknown 1Password alias "${alias}". Configure refs.${alias}.`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {object} params - { reference, keyPath, alias }
|
|
195
|
+
* @returns {string} Deterministic placeholder (never links or secrets)
|
|
196
|
+
*/
|
|
197
|
+
function placeholderFor({ reference, keyPath, alias }) {
|
|
198
|
+
if (alias) {
|
|
199
|
+
return `[OP:alias:${alias}${keyPath ? `.${keyPath}` : ''}]`
|
|
200
|
+
}
|
|
201
|
+
if (reference.kind === 'secretRef') {
|
|
202
|
+
return `[OP:secretRef:${reference.ref}]`
|
|
203
|
+
}
|
|
204
|
+
if (reference.kind === 'privateLink') {
|
|
205
|
+
return `[OP:privateLink:${reference.item}]`
|
|
206
|
+
}
|
|
207
|
+
return `[OP:item:${reference.item}${keyPath ? `:${keyPath}` : ''}]`
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Fetch and select the backing field value for a normalized reference.
|
|
212
|
+
* For items without a configured field, the first key path segment may
|
|
213
|
+
* select a field directly: ${op(My Login).password} picks the password
|
|
214
|
+
* field, while ${op(note-item).NPM_TOKEN} falls back to inference and
|
|
215
|
+
* treats NPM_TOKEN as an INI key inside the inferred field.
|
|
216
|
+
* @param {object} reference - Normalized reference
|
|
217
|
+
* @param {string|undefined} keyPath - Requested key path
|
|
218
|
+
* @param {string} [label] - Human label for the auth hint
|
|
219
|
+
* @returns {Promise<{value: string, fieldName: string, remainingKeyPath: string|undefined}>} Selected value
|
|
220
|
+
*/
|
|
221
|
+
async function fetchValue(reference, keyPath, label) {
|
|
222
|
+
if (reference.kind === 'secretRef') {
|
|
223
|
+
const value = await cached(secretRefCache, `${scopeKey}|${reference.ref}`, () => {
|
|
224
|
+
return readSecretRef(reference.ref, cliOptions)
|
|
225
|
+
}, label)
|
|
226
|
+
return { value, fieldName: reference.ref, remainingKeyPath: keyPath }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const vault = reference.vault
|
|
230
|
+
const itemKey = `${scopeKey}|${vault || ''}|${reference.item}`
|
|
231
|
+
const item = await cached(itemCache, itemKey, () => {
|
|
232
|
+
return getItem(reference.item, { ...cliOptions, vault })
|
|
233
|
+
}, label)
|
|
234
|
+
|
|
235
|
+
// Field selection is a pure function over the cached item JSON, so it
|
|
236
|
+
// needs no cache of its own - no op call is ever saved by one.
|
|
237
|
+
if (reference.field) {
|
|
238
|
+
const field = selectField(item, { field: reference.field, section: reference.section })
|
|
239
|
+
return { value: field.value, fieldName: field.label || field.id, remainingKeyPath: keyPath }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (keyPath !== undefined) {
|
|
243
|
+
const dotIndex = keyPath.indexOf('.')
|
|
244
|
+
const firstSegment = dotIndex === -1 ? keyPath : keyPath.slice(0, dotIndex)
|
|
245
|
+
const fieldMatch = trySelectField(item, { field: firstSegment })
|
|
246
|
+
if (fieldMatch) {
|
|
247
|
+
const remaining = dotIndex === -1 ? undefined : keyPath.slice(dotIndex + 1)
|
|
248
|
+
return { value: fieldMatch.value, fieldName: fieldMatch.label || fieldMatch.id, remainingKeyPath: remaining }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const field = selectField(item, {})
|
|
253
|
+
return { value: field.value, fieldName: field.label || field.id, remainingKeyPath: keyPath }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {string} varString - Variable body without ${}
|
|
258
|
+
* @param {object} opts - Resolver options from core (unused)
|
|
259
|
+
* @param {object} currentObject - Config object being populated (unused)
|
|
260
|
+
* @param {object} valueObject - Core value context ({ originalSource, path })
|
|
261
|
+
* @returns {Promise<string>} Resolved value or placeholder
|
|
262
|
+
*/
|
|
263
|
+
async function resolver(varString, opts, currentObject, valueObject) {
|
|
264
|
+
const parsed = parseVariable(varString)
|
|
265
|
+
const { reference, keyPath, alias } = parsed
|
|
266
|
+
const configPath = valueObject && valueObject.path ? valueObject.path.join('.') : undefined
|
|
267
|
+
|
|
268
|
+
// Direct private links put the full URL (with account/host params) in
|
|
269
|
+
// the variable string itself - redact it from opReferences entries.
|
|
270
|
+
const isDirectLink = reference.kind === 'privateLink' && !alias
|
|
271
|
+
const redacted = `\${op(...)${keyPath ? `.${keyPath}` : ''}}`
|
|
272
|
+
const entry = {
|
|
273
|
+
raw: isDirectLink ? redacted : (valueObject ? valueObject.originalSource : `\${${varString}}`),
|
|
274
|
+
resolved: isDirectLink ? redacted : `\${${varString}}`,
|
|
275
|
+
alias,
|
|
276
|
+
referenceKind: reference.kind,
|
|
277
|
+
ref: reference.kind === 'secretRef' ? reference.ref : undefined,
|
|
278
|
+
item: reference.item,
|
|
279
|
+
vault: reference.vault,
|
|
280
|
+
field: reference.field,
|
|
281
|
+
section: reference.section,
|
|
282
|
+
keyPath,
|
|
283
|
+
configPath,
|
|
284
|
+
sensitive: true,
|
|
285
|
+
risk: 'remote_secret_read',
|
|
286
|
+
source: 'remote',
|
|
287
|
+
skipped: skipResolution === true,
|
|
288
|
+
}
|
|
289
|
+
if (reference.warnings && reference.warnings.length) {
|
|
290
|
+
entry.diagnostics = reference.warnings.map((warning) => ({ ...warning, configPath }))
|
|
291
|
+
}
|
|
292
|
+
opReferences.push(entry)
|
|
293
|
+
|
|
294
|
+
if (skipResolution) {
|
|
295
|
+
return placeholderFor(parsed)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Auth-hint label: what the user recognizes — the alias, else the config
|
|
299
|
+
// key being set, else the field. Never a secret value.
|
|
300
|
+
const label = alias
|
|
301
|
+
|| (configPath ? configPath.split('.').pop() : undefined)
|
|
302
|
+
|| reference.field
|
|
303
|
+
|| (reference.kind === 'secretRef' ? reference.ref : reference.item)
|
|
304
|
+
const { value, fieldName, remainingKeyPath } = await fetchValue(reference, keyPath, label)
|
|
305
|
+
if (entry.field === undefined && reference.kind !== 'secretRef') {
|
|
306
|
+
entry.field = fieldName
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (remainingKeyPath === undefined) {
|
|
310
|
+
return value
|
|
311
|
+
}
|
|
312
|
+
const parsedValue = parseStructuredSecret(value, { fieldName })
|
|
313
|
+
return getKeyPath(parsedValue, remainingKeyPath, fieldName)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
type: OP_PREFIX,
|
|
318
|
+
source: 'remote',
|
|
319
|
+
prefix: OP_PREFIX,
|
|
320
|
+
syntax: '${op:alias.KEY}, ${op(item).KEY}, or ${op(op://vault/item/field)}',
|
|
321
|
+
description: 'Resolves values from 1Password through the op CLI',
|
|
322
|
+
sensitive: true,
|
|
323
|
+
risk: 'remote_secret_read',
|
|
324
|
+
match: opVariableSyntax,
|
|
325
|
+
resolver,
|
|
326
|
+
metadataKey: 'opReferences',
|
|
327
|
+
collectMetadata: () => opReferences,
|
|
328
|
+
clearCache: () => {
|
|
329
|
+
secretRefCache.clear()
|
|
330
|
+
itemCache.clear()
|
|
331
|
+
opReferences.length = 0
|
|
332
|
+
coldStartCall = null
|
|
333
|
+
hintScheduled = false
|
|
334
|
+
pendingLabels.length = 0
|
|
335
|
+
},
|
|
336
|
+
syncFactory: require.resolve('./sync-factory'),
|
|
337
|
+
syncOptions: buildSyncOptions(),
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @returns {object} JSON-serializable options for the sync worker
|
|
342
|
+
*/
|
|
343
|
+
function buildSyncOptions() {
|
|
344
|
+
const syncOptions = { refs, account, configDir, opPath, skipResolution }
|
|
345
|
+
if (execFile) {
|
|
346
|
+
// Functions cannot cross the JSON boundary into the sync worker;
|
|
347
|
+
// flag it so sync-factory can fail loudly instead of silently
|
|
348
|
+
// running the real op binary.
|
|
349
|
+
syncOptions.hasInjectedExecFile = true
|
|
350
|
+
}
|
|
351
|
+
return syncOptions
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
module.exports = createOnePasswordResolver
|
|
356
|
+
module.exports.opVariableSyntax = opVariableSyntax
|