@ciqol/resenv 0.1.0-beta.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 (65) hide show
  1. package/CONTRIBUTING.md +189 -0
  2. package/LICENSE +21 -0
  3. package/README.md +243 -0
  4. package/SECURITY.md +74 -0
  5. package/assert-safe-diagnostics.cjs +15 -0
  6. package/bin/resenv.cjs +91 -0
  7. package/bin/resenv.mjs +1 -0
  8. package/commands.cjs +85 -0
  9. package/docs/build.md +136 -0
  10. package/docs/dependencies.md +505 -0
  11. package/docs/doppler.md +146 -0
  12. package/docs/maintainer-handoff.md +112 -0
  13. package/docs/maturity.md +61 -0
  14. package/docs/public-readiness.md +138 -0
  15. package/docs/release-security.md +134 -0
  16. package/docs/releasing.md +143 -0
  17. package/docs/security-exceptions.json +139 -0
  18. package/docs/security-performance-review.md +150 -0
  19. package/docs/validation.md +700 -0
  20. package/doppler/auth.cjs +125 -0
  21. package/doppler/bridge.cjs +66 -0
  22. package/doppler/http.cjs +77 -0
  23. package/doppler/references.cjs +28 -0
  24. package/doppler/resolve.cjs +58 -0
  25. package/doppler/system.cjs +99 -0
  26. package/doppler-worker.cjs +28 -0
  27. package/doppler-worker.mjs +1 -0
  28. package/errors.cjs +12 -0
  29. package/index.cjs +88 -0
  30. package/index.d.cts +1 -0
  31. package/index.d.mts +1 -0
  32. package/index.mjs +1 -0
  33. package/package.json +89 -0
  34. package/preload.cjs +31 -0
  35. package/resolve-commands.cjs +5 -0
  36. package/resolve-commands.d.cts +1 -0
  37. package/resolve-commands.d.mts +1 -0
  38. package/resolve-commands.mjs +1 -0
  39. package/resolve-doppler.cjs +4 -0
  40. package/resolve-doppler.d.cts +1 -0
  41. package/resolve-doppler.d.mts +1 -0
  42. package/resolve-doppler.mjs +1 -0
  43. package/resolve.cjs +5 -0
  44. package/resolve.d.cts +1 -0
  45. package/resolve.d.mts +1 -0
  46. package/resolve.mjs +1 -0
  47. package/types/bin/resenv.d.ts +2 -0
  48. package/types/src/assert-safe-diagnostics.d.ts +1 -0
  49. package/types/src/commands.d.ts +4 -0
  50. package/types/src/doppler/auth.d.ts +19 -0
  51. package/types/src/doppler/bridge.d.ts +4 -0
  52. package/types/src/doppler/http.d.ts +9 -0
  53. package/types/src/doppler/references.d.ts +9 -0
  54. package/types/src/doppler/resolve.d.ts +9 -0
  55. package/types/src/doppler/system.d.ts +24 -0
  56. package/types/src/doppler-worker.d.ts +1 -0
  57. package/types/src/errors.d.ts +5 -0
  58. package/types/src/index.d.ts +7 -0
  59. package/types/src/preload.d.ts +4 -0
  60. package/types/src/resolve-commands.d.ts +1 -0
  61. package/types/src/resolve-doppler.d.ts +1 -0
  62. package/types/src/resolve.d.ts +1 -0
  63. package/types/src/types.d.ts +9 -0
  64. package/types/src/values.d.ts +5 -0
  65. package/values.cjs +20 -0
@@ -0,0 +1,189 @@
1
+ # Contributing
2
+
3
+ Starting a new session? Read the [maintainer handoff](docs/maintainer-handoff.md)
4
+ and [repository instructions](https://github.com/ciqol/resenv/blob/beta/AGENTS.md) before editing or releasing.
5
+
6
+ The [README](README.md) covers using resenv in an application. This guide covers
7
+ changing resenv itself and verifying those changes.
8
+
9
+ Releases start at `0.1.0-beta.1` on the sole branch `beta`, using npm channel
10
+ `beta`. Semantic-release's official commit-analysis and release-notes plugins
11
+ select releasable changes and generate notes; actual beta tags supply the next
12
+ prerelease counter. The root and ordinary local builds remain private prototypes.
13
+ CI generates the publishable manifest before packing and testing it, then stages
14
+ the exact archive through npm OIDC for maintainer approval. See
15
+ [releasing](docs/releasing.md) and the bounded [release-tool review](docs/release-security.md).
16
+ The source uses the [MIT license](LICENSE), copyright ciqol contributors.
17
+
18
+ ## Local development
19
+
20
+ Use the Node version in `.nvmrc` and npm:
21
+
22
+ ```sh
23
+ nvm use
24
+ npm ci
25
+ npm run verify
26
+ ```
27
+
28
+ `npm run verify` is the complete local check: lint, strict source/spec type
29
+ checks, build, unit/process tests and installed-package checks. To build without
30
+ running the checks, use `npm run build`.
31
+
32
+ After building, try the CLI from this checkout:
33
+
34
+ ```sh
35
+ node dist/bin/resenv.cjs --help
36
+ ```
37
+
38
+ Author library and CLI code as `.ts`; Rollup's TypeScript plugin produces both
39
+ module formats and declarations from that implementation. Colocate Vitest unit
40
+ specs as `*.spec.ts` beside each tested module and import its source. Build and
41
+ release scripts have colocated `*.spec.mjs` Vitest specs. Keep
42
+ process, package and integration checks in `test/` as `.mjs`; they exercise
43
+ compiled output and installed packages. Tests use dummy credentials and commands.
44
+ Never use real secrets in fixtures, terminal output, screenshots, issues, or
45
+ pull requests.
46
+
47
+ Provider detection belongs to the original environment values, including library
48
+ calls without options. Keep shell execution behind `commands: true`, preserve
49
+ literal fetched output, and validate references before any command runs.
50
+
51
+ ESLint applies rules that use type information to the TypeScript implementation and specs
52
+ and checks the JavaScript build tooling.
53
+ Use `npm run lint:fix` for available automatic fixes; preserve the repository's
54
+ existing compact formatting. Public object shapes use `type` with `readonly`
55
+ properties. `Environment` is intentionally mutable because in-place resolution
56
+ updates it. `npm run typecheck` checks the entire library and CLI with `strict`
57
+ and `noUncheckedIndexedAccess`. A separate spec configuration checks tests without
58
+ shipping them in JavaScript or declarations. Do not maintain separate handwritten declarations.
59
+
60
+ ```sh
61
+ npm run test:unit # colocated Vitest specs, no build required
62
+ npm run test:watch # watch unit specs while editing
63
+ npm run test:coverage # source unit coverage, text and lcov output
64
+ npm run test:integration # process checks, requires a build
65
+ ```
66
+
67
+ `npm test` runs unit and process tests. Mock or inject provider calls, credential
68
+ helpers, browser launchers and process control in unit tests. Keep checks that
69
+ spawn actual commands or temporary applications in `test/`. The unit coverage
70
+ report measures source and is separate from packed-package and process checks.
71
+
72
+ Use normal npm lifecycle behavior locally and in CI. Review install hooks,
73
+ including implicit native builds, when changing dependencies; record any
74
+ required setup in [the dependency review](docs/dependencies.md). Suppressing
75
+ lifecycle hooks can skip required setup, so it is not part of the standard
76
+ installation commands. The explicit build/check commands remain required.
77
+
78
+ The package check packs `dist/` and installs that tarball into a temporary
79
+ consumer outside the repository. The install is offline, uses npm's existing
80
+ cache and the repository's reviewed runtime lock entries. It installs with
81
+ `npm ci` without needing cached registry metadata. It does not link the source
82
+ tree or publish. Run `npm ci` first to populate the archive cache. The check
83
+ exercises both module formats, CLI and preloads, shared error identity, single
84
+ preload execution and `.cts`/`.mts` consumer types.
85
+
86
+ Pass an existing archive to test the same build CI uses:
87
+ `npm run test:package -- /absolute/path/to/package.tgz`. For a final CI candidate,
88
+ also pass its `release.json` as the second argument. The supplied package's
89
+ name, version, executable mode and manifest are checked before runtime probes.
90
+ `npm run verify` runs lint, typecheck, build and tests; `npm run release:check`
91
+ also previews the package contents. Neither publishes or creates a tag.
92
+
93
+ Rebuild after source, declarations, metadata or documentation changes. The build
94
+ cleans `dist/` first and generates its manifest from the root package. See
95
+ [docs/build.md](docs/build.md). Ordinary local builds remain private. Only a validated CI release plan removes
96
+ that guard from the distribution manifest; never edit it by hand.
97
+
98
+ ## Try changes in an application
99
+
100
+ ### Install a local archive
101
+
102
+ From the resenv checkout:
103
+
104
+ ```sh
105
+ npm run build
106
+ npm run pack:local
107
+ ```
108
+
109
+ Install the resulting archive from the application's directory:
110
+
111
+ ```sh
112
+ npm install --save-exact /absolute/path/to/ciqol-resenv-0.1.0-beta.1.tgz
113
+ ```
114
+
115
+ This changes the application's manifest and lockfile. The archive must exist
116
+ wherever it installs dependencies. Use a new archive version for changes to an
117
+ already shared build; never overwrite an archive another project uses. Root
118
+ package overrides do not propagate to consumers; inspect the installed tree
119
+ against [the dependency review](docs/dependencies.md).
120
+
121
+ ### Try local changes without changing the application's CI
122
+
123
+ Link this checkout's built `dist/` directory at the application's ignored
124
+ `node_modules/@ciqol/resenv`, retaining any existing installation first. Keep
125
+ references in an ignored `.env.serve.local` and the application's target edit
126
+ local. Leave its manifest, lockfile and CI configuration unchanged.
127
+
128
+ Rebuild resenv after source, metadata or documentation changes and restart the
129
+ application. An application `npm ci` can remove the link; recreate it when needed.
130
+ If you only created the package link, run its CLI directly:
131
+
132
+ ```sh
133
+ node node_modules/@ciqol/resenv/bin/resenv.cjs login doppler
134
+ ```
135
+
136
+ For login from the resenv checkout, use
137
+ `node dist/bin/resenv.cjs login doppler`. Real login is a manual integration
138
+ check; automated tests must use injected or mocked credentials.
139
+
140
+ ## Optional integration checks
141
+
142
+ `npm run bench` measures fresh-process startup, a plain 200-variable environment,
143
+ and a mocked provider with eight distinct lookups. It needs a build first and
144
+ does not contact a provider. Timings are observations, not CI pass/fail thresholds.
145
+ See [the review](docs/security-performance-review.md) for the recorded comparison.
146
+
147
+ With an existing Nx installation:
148
+
149
+ ```sh
150
+ npm run test:nx -- /absolute/path/to/workspace/node_modules
151
+ ```
152
+
153
+ This creates a temporary Nx workspace and checks target-file loading,
154
+ precedence and secret resolution with dummy values. It does not use the
155
+ source workspace's dotenv files, credentials or remote cache.
156
+
157
+ ## Changes
158
+
159
+ Keep dotenv file discovery with the caller. Doppler is the only built-in cloud
160
+ provider; its authentication and lookups live under `src/doppler/`.
161
+ Preserve all-or-nothing environment updates, strict failures, explicit command
162
+ opt-in and literal handling of resolved values. Update the public API types and
163
+ documentation together with behavior changes.
164
+
165
+ Keep one public resolver, `resolve()`. It always returns an independent
166
+ resolved copy and replaces input values only with `{ replace: true }`. Preloads
167
+ enable that option; the CLI passes the returned copy to its child process.
168
+
169
+ Add regression tests for changed behavior and avoid real provider calls. Use
170
+ Conventional Commit subjects, for example `fix(resolver): preserve empty values`.
171
+ Dependency changes require an updated [review record](docs/dependencies.md).
172
+
173
+ CODEOWNERS identifies the maintainer. Keep bug reports and pull requests focused
174
+ on observable behavior and include the checks run. Weekly dependency updates are
175
+ grouped, with major version updates reviewed separately; bots cannot merge them.
176
+
177
+ Keep the README focused on installation, authentication, application integration,
178
+ API behavior and troubleshooting. Put contributor tooling, test commands and
179
+ local development instructions here; detailed internals belong in `docs/`.
180
+
181
+ Security reports follow [SECURITY.md](SECURITY.md). Publication and repository
182
+ visibility changes require a separate maintainer decision; see the
183
+ [release checklist](docs/releasing.md).
184
+
185
+ npm releases run only through GitHub Actions with npm trusted publishing (OIDC).
186
+ The maintainer configures the npm side. Local development and release checks
187
+ do not require `npm login`; do not publish from a developer machine or add npm
188
+ publishing tokens. CI must publish the exact candidate archive that its tests
189
+ verified.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ciqol contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # @ciqol/resenv
2
+
3
+ **Replace references already in `process.env` with their resolved values at
4
+ application startup.** Your existing loader, such as Nx, chooses the dotenv
5
+ files. resenv resolves the resulting environment before application code runs.
6
+
7
+ **Doppler is the only built-in cloud provider.** A value such as
8
+ `doppler://my-project/dev/API_TOKEN` tells resenv which secret to fetch. The
9
+ application then reads the actual value from `process.env.API_TOKEN`.
10
+
11
+ ```text
12
+ Nx or another loader sets process.env
13
+ |
14
+ resenv fetches the referenced values and replaces them in memory
15
+ |
16
+ Application starts with ordinary environment values
17
+ ```
18
+
19
+ Resolution happens once at startup. It does not watch the provider for changes;
20
+ restart the application to fetch updated values. No filename is passed to
21
+ resenv, and dotenv files are never rewritten.
22
+
23
+ Requires Node.js 20 or later. This is an experimental beta, currently distributed
24
+ as a package archive; it has not been published to npm.
25
+
26
+ ## Install
27
+
28
+ Install the archive supplied by the maintainer in your application project:
29
+
30
+ ```sh
31
+ npm install --save-exact /path/to/ciqol-resenv-0.1.0-beta.1.tgz
32
+ ```
33
+
34
+ This records resenv in your project's manifest and lockfile. Keep the archive
35
+ available wherever that project installs dependencies. Once the beta is
36
+ published to npm, you will be able to install `@ciqol/resenv@beta` instead.
37
+
38
+ The CLI examples below use `npx --no-install` to run the copy installed in your
39
+ project. In an npm script, use `resenv` directly.
40
+
41
+ ## Use with Doppler
42
+
43
+ ### 1. Authenticate locally
44
+
45
+ After installing the package, run:
46
+
47
+ ```sh
48
+ npx --no-install resenv login doppler
49
+ ```
50
+
51
+ Each developer authenticates with their own Doppler access. Secret reads use
52
+ the HTTPS API. Credentials come from `DOPPLER_TOKEN`, the saved OS credential,
53
+ or an existing Doppler CLI login, in that order. Startup reports missing access
54
+ and stops; it does not open a login prompt.
55
+
56
+ Native login uses the browser flow observed in Doppler's official MCP client
57
+ and saves the token in macOS Keychain or Linux Secret Service. The CLI is a
58
+ fallback; the browser protocol is not a public OAuth contract and fresh native
59
+ login is not yet verified end to end. Windows needs `DOPPLER_TOKEN` because
60
+ native login/storage is not implemented there. See
61
+ [Doppler login and platform details](docs/doppler.md).
62
+
63
+ ### 2. Declare references in the environment your app already loads
64
+
65
+ For example, in an Nx application's `.env.serve`:
66
+
67
+ ```dotenv
68
+ API_TOKEN=doppler://my-project/dev/API_TOKEN
69
+ PORT=3000
70
+ ```
71
+
72
+ The reference contains the Doppler project, config and secret name. This is
73
+ resenv's reference syntax. Plain values such as `PORT` pass through unchanged.
74
+ The reference itself grants no access: the authenticated identity needs
75
+ permission to read that secret. Doppler references need no dotenvx private key.
76
+
77
+ ### 3. Preload resenv on the application process
78
+
79
+ Once the environment has been loaded:
80
+
81
+ ```sh
82
+ node --require @ciqol/resenv/resolve app.cjs
83
+ node --import @ciqol/resenv/resolve app.mjs
84
+ ```
85
+
86
+ The preload replaces `process.env.API_TOKEN` before the application and its
87
+ imports execute. It infers Doppler from the value, so no provider flag is needed.
88
+
89
+ Alternatively, wrap a command whose environment is already populated:
90
+
91
+ ```sh
92
+ npx --no-install resenv run -- node app.mjs
93
+ ```
94
+
95
+ The wrapper supplies resolved values to the child process. It inherits
96
+ stdin/stdout/stderr, preserves exit status and forwards SIGINT, SIGTERM and
97
+ SIGHUP to that child.
98
+
99
+ ## Nx integration
100
+
101
+ Put the preload on the application's target, after Nx loads its dotenv files:
102
+
103
+ ```json
104
+ {
105
+ "name": "app",
106
+ "targets": {
107
+ "serve": {
108
+ "executor": "nx:run-commands",
109
+ "cache": false,
110
+ "continuous": true,
111
+ "options": {
112
+ "command": "node --require=@ciqol/resenv/resolve app.mjs",
113
+ "cwd": "{projectRoot}"
114
+ }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ Run `nx serve app`. Nx selects `.env.serve` and any applicable overrides,
121
+ then starts the application with resenv. Use your application's existing path
122
+ and target options. For an existing `@nx/js:node` target, add:
123
+
124
+ ```json
125
+ "runtimeArgs": ["--require", "@ciqol/resenv/resolve"]
126
+ ```
127
+
128
+ Preloading the outer `nx` process runs too early. A shell-wide `NODE_OPTIONS`
129
+ setting also affects unrelated processes. Keep the preload on the application
130
+ target. If a build needs secrets, its process needs its own integration after
131
+ its environment is loaded; values embedded in build output remain in that output.
132
+
133
+ ## Resolution behavior
134
+
135
+ - Only the enabled reference forms are resolved. Ordinary values pass through.
136
+ - Doppler fetches only the named secrets; repeated references share one request
137
+ within a resolution pass. Values are not cached on disk.
138
+ - All lookups must succeed before the environment is changed or a child starts.
139
+ Missing access, malformed references and provider failures stop startup.
140
+ - Fetched values stay literal, even if they contain `$()` or another reference.
141
+ - The canonical preload leaves shell expressions literal. Executing commands
142
+ requires the separate opt-in described below.
143
+ - Application code can read its resolved environment. Access to the same
144
+ credentials also permits other code or sessions to retrieve those secrets.
145
+
146
+ See [Doppler limits](docs/doppler.md#resolution-and-security-boundaries) and
147
+ [security guidance](SECURITY.md).
148
+
149
+ ## Library API
150
+
151
+ Call the library after your loader has populated the environment, before
152
+ importing modules that read configuration:
153
+
154
+ ```js
155
+ import { resolve } from '@ciqol/resenv'
156
+
157
+ const childEnv = resolve()
158
+
159
+ // Also replace values in process.env; the returned copy can be ignored.
160
+ resolve(process.env, { replace: true })
161
+ ```
162
+
163
+ `resolve(env = process.env, options = {})` always returns a new resolved
164
+ environment. `replace` defaults to `false`, leaving the input unchanged. With
165
+ `replace: true`, it also replaces values in the input after every lookup succeeds;
166
+ the return value is still an independent copy.
167
+
168
+ Providers are inferred from each original value, just as with `/resolve` and
169
+ `run`. Doppler is the only supported cloud provider today. No options are
170
+ required: without references, the result is an unchanged copy and no credentials
171
+ are accessed. An environment can contain several references, including different
172
+ Doppler projects/configs. Each value selects its own source; a command can supply
173
+ a Doppler token. Commands still require
174
+ `commands: true`. Returned secret values are never interpreted again.
175
+
176
+ CommonJS uses the same API through
177
+ `require('@ciqol/resenv')`. Both formats share the `ResenvError` class, whose
178
+ `code` identifies the failure. CLI and preload failures exit with a safe message.
179
+
180
+ Using `replace: true` requires `process.env` or writable data properties. A read-only
181
+ property, setter or Proxy target fails before any replacement is committed.
182
+ Keep the default copy mode for inputs that should remain unchanged. The Node
183
+ preloads enable in-place replacement so application imports see resolved values.
184
+
185
+ ## Debug startup
186
+
187
+ ```sh
188
+ NODE_DEBUG=ciqol:resenv node --require @ciqol/resenv/resolve app.mjs
189
+ ```
190
+
191
+ For the Nx target above, use `NODE_DEBUG=ciqol:resenv nx serve app`. Set `NODE_DEBUG`
192
+ when launching Node, before dotenv loading. Diagnostics use `node:util`'s
193
+ `debuglog` and are off by default:
194
+
195
+ ```text
196
+ CIQOL:RESENV 12345: Replaced secret value for "API_TOKEN".
197
+ CIQOL:RESENV 12345: Done: replaced 1 secret value(s).
198
+ ```
199
+
200
+ With no matching references, the only message is `Nothing to handle.` Failures
201
+ include a safe error and completion summary. Logs omit values, tokens, command
202
+ text and provider responses; no log files are created.
203
+
204
+ `NODE_DEBUG=child_process` (including `NODE_DEBUG=*`) makes Node print spawn
205
+ options containing credentials. resenv blocks its subprocess operations with
206
+ `UNSAFE_DEBUG_LOGGING` in that mode. Use `NODE_DEBUG=ciqol:resenv` instead.
207
+
208
+ ## Optional commands
209
+
210
+ - **Opt-in shell commands:** `/resolve-commands` resolves complete `$(command)`
211
+ values through `/bin/sh`. The canonical `/resolve` preload leaves these values
212
+ literal for both `--require` and `--import`, including when `RESENV_COMMANDS`
213
+ is set in the environment. `resenv run --commands -- ...` enables commands
214
+ alongside inferred Doppler references, as does the command preload. Use only
215
+ trusted configuration; commands run with your permissions. A timeout limits
216
+ execution time, but cannot undo file changes, network requests or other effects.
217
+ Leave this option disabled when the environment can contain untrusted input.
218
+ Commands have closed stdin, a 30-second timeout and a 64 KiB combined output
219
+ limit. Trailing LF characters are removed; fetched output stays literal.
220
+ Embedded forms such as `prefix$(command)suffix` are unsupported.
221
+
222
+ Values beginning with `encrypted:` are left unchanged. resenv resolves cloud
223
+ references; dotenvx decryption is outside its scope. The package has no runtime
224
+ npm dependencies.
225
+
226
+ `/resolve-doppler` is an equivalent name for `/resolve`; `run --doppler` remains
227
+ accepted. New integrations should use the canonical entry points above.
228
+
229
+ Use one preload mode per process. Combining `/resolve` and `/resolve-commands`
230
+ or combining a preload with `resenv run` fails with `CONFLICTING_PRELOADS` to
231
+ prevent reinterpreting fetched values. Do not configure a second resolver in
232
+ the child process through `NODE_OPTIONS` or other launch hooks.
233
+ For both modes together, use `resenv run --commands -- ...` or one library call
234
+ with `{ commands: true }`.
235
+
236
+ ## Contributing
237
+
238
+ For checkout setup, building, testing, trying local changes in another project
239
+ and submitting fixes, see [CONTRIBUTING.md](CONTRIBUTING.md).
240
+
241
+ ## License
242
+
243
+ [MIT](LICENSE). Copyright (c) 2026 ciqol contributors.
package/SECURITY.md ADDED
@@ -0,0 +1,74 @@
1
+ # Security
2
+
3
+ ## Scope and trust
4
+
5
+ resenv resolves values in a process environment. It does not provide a vault,
6
+ access-control service, key rotation, or isolation between programs running
7
+ with the same credentials. The application can read its resolved secrets and
8
+ any credentials supplied to it.
9
+
10
+ Doppler mode uses a fixed HTTPS API origin, disables redirects and fetches only
11
+ referenced secret names. It does not execute shell expressions. Credentials
12
+ come from an explicit token, resenv's OS credential store or a fixed Doppler CLI
13
+ helper. An API rejection never triggers a switch to another identity. Native
14
+ browser login uses endpoints observed in Doppler's official MCP client rather
15
+ than a public third-party OAuth contract. See [Doppler details](docs/doppler.md)
16
+ for fallback rules, credential scope and platform limits.
17
+ The canonical `resolve` preload and `run` command select Doppler only for
18
+ `doppler://` values. They do not infer shell execution from `$()` values.
19
+ Startup does not initiate login; the separate login command rejects CI and
20
+ noninteractive sessions.
21
+
22
+ The local credential store does not prevent code running as the same user from
23
+ requesting credentials. resenv does not automatically remove an explicitly
24
+ supplied `DOPPLER_TOKEN` from the application environment. Interactive login
25
+ displays a temporary human authorization code; tokens and the polling code are
26
+ not logged. The Node preload's private worker exchanges credentials and results
27
+ over captured pipes, never process arguments or dotenv files.
28
+
29
+ Command mode executes trusted configuration with the current user's
30
+ permissions. Enable it only for configuration you have reviewed. Commands can
31
+ access files and the network; failed resolution cannot undo their external
32
+ effects. Commands are bounded by timeout and output limits, and their output
33
+ is not included in resenv diagnostics.
34
+
35
+ Node's `child_process` debug category prints spawn options, including stdin
36
+ and environment values. resenv refuses its own subprocess and credential-helper
37
+ operations when that category is enabled, including through `NODE_DEBUG=*`.
38
+ Use `NODE_DEBUG=ciqol:resenv` for diagnostics that omit values. This guard does not
39
+ control application logs, other instrumentation, or a debugger attached to the
40
+ process.
41
+
42
+ All values are resolved before resenv updates the caller's environment or
43
+ starts the application. Provider failures, command failures and invalid
44
+ environment values stop startup. Fetched values are never evaluated recursively. Application logs are outside resenv's control.
45
+
46
+ Different automatic preload modes cannot be combined in one process. Otherwise
47
+ the second mode could execute literal data fetched by the first.
48
+ Combining a preload with the CLI is rejected for the same reason. Do not add
49
+ another resolver to the child through `NODE_OPTIONS` or other launch hooks.
50
+ Use one `resenv run --commands` invocation or one library call with commands
51
+ enabled to resolve cloud references and commands together. Repeated manual
52
+ library calls are separate operations; only pass original configuration to them.
53
+
54
+ Provider responses must be valid UTF-8 and JSON, and resolved strings must not
55
+ contain lone Unicode surrogates or null bytes. In-place replacement preflights
56
+ all changed properties; read-only properties, setters and Proxy targets are
57
+ rejected before any values are committed. Use `resolve()` to obtain a copy
58
+ when the input is not a writable environment record.
59
+
60
+ This experimental package has not had an independent security audit. Validation
61
+ evidence and platform limits are recorded in [docs/validation.md](docs/validation.md).
62
+
63
+ ## Reporting a vulnerability
64
+
65
+ Do not open a public issue containing a vulnerability, credentials or sensitive
66
+ output. Open the repository's Security tab and use "Report a vulnerability"
67
+ when available. If that option is unavailable, contact the maintainer through
68
+ an existing private channel. Before the public transition is complete,
69
+ maintainers must enable GitHub private vulnerability reporting and verify that
70
+ it is available.
71
+
72
+ Include affected versions, platform, impact and a minimal reproduction using
73
+ dummy data. Never include production tokens, private keys or environment dumps.
74
+ There is no guaranteed response time or long-term support policy yet.
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ var node_util = require('node:util');
4
+ var errors = require('./errors.cjs');
5
+
6
+ const childProcessDebug = node_util.debuglog('child_process');
7
+ function assertSafeDiagnostics() {
8
+ // Node logs spawn options, including stdin and env, before starting a child.
9
+ // Test Node's actual debug state, including wildcard selectors and caching.
10
+ if (childProcessDebug.enabled) {
11
+ throw new errors.ResenvError('UNSAFE_DEBUG_LOGGING', 'Disable child_process in NODE_DEBUG before resolving secrets or starting a child. Use NODE_DEBUG=ciqol:resenv for value-free diagnostics.');
12
+ }
13
+ }
14
+
15
+ exports.assertSafeDiagnostics = assertSafeDiagnostics;
package/bin/resenv.cjs ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var node_child_process = require('node:child_process');
5
+ var node_os = require('node:os');
6
+ var assertSafeDiagnostics = require('../assert-safe-diagnostics.cjs');
7
+ var preload = require('../preload.cjs');
8
+ var index = require('../index.cjs');
9
+ var auth = require('../doppler/auth.cjs');
10
+ var system = require('../doppler/system.cjs');
11
+ var references = require('../doppler/references.cjs');
12
+ var values = require('../values.cjs');
13
+
14
+ const usage = 'Usage: resenv run [--commands] -- <command> [arguments...]\n resenv login [doppler] [--cli] [--no-browser]\n resenv logout doppler\nInfer secret providers from values already in the environment.\n--doppler remains accepted for compatibility.\n--commands enables whole-value $(command) expressions using /bin/sh.\n';
15
+ async function main() {
16
+ const args = process.argv.slice(2);
17
+ if (args.length === 1 && ['--help', '-h'].includes(args[0] || '')) {
18
+ process.stdout.write(usage);
19
+ return;
20
+ }
21
+ const explicitProvider = args[1] === 'doppler';
22
+ if (args[0] === 'login' && args.slice(explicitProvider ? 2 : 1).every(arg => ['--cli', '--no-browser'].includes(arg))) {
23
+ if (!explicitProvider && !references.references(process.env).length) {
24
+ process.stderr.write('Nothing to handle: no cloud secret references in the environment. Load your target environment first, or use resenv login doppler.\n');
25
+ return;
26
+ }
27
+ await auth.login({ cli: args.includes('--cli'), noBrowser: args.includes('--no-browser') });
28
+ return;
29
+ }
30
+ if (args.length === 2 && args[0] === 'logout' && args[1] === 'doppler') {
31
+ system.createSystem().remove();
32
+ process.stderr.write('Removed the resenv credential. Doppler CLI credentials and DOPPLER_TOKEN are unchanged; revoke the token in Doppler to invalidate it.\n');
33
+ return;
34
+ }
35
+ let commands = false;
36
+ while (args[0] === 'run' && ['--commands', '--doppler'].includes(args[1] || '')) {
37
+ if (args[1] === '--commands')
38
+ commands = true;
39
+ args.splice(1, 1);
40
+ }
41
+ const [operation, separator, command, ...commandArgs] = args;
42
+ if (operation !== 'run' || separator !== '--' || !command) {
43
+ process.stderr.write(usage);
44
+ process.exitCode = 2;
45
+ return;
46
+ }
47
+ let env;
48
+ try {
49
+ preload.assertNoPreload();
50
+ env = index.resolve(process.env, { commands });
51
+ }
52
+ catch (err) {
53
+ process.stderr.write(`${index.formatError(err)}\n`);
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ assertSafeDiagnostics.assertSafeDiagnostics();
58
+ const child = node_child_process.spawn(command, commandArgs, { env, stdio: 'inherit', shell: false });
59
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
60
+ const handlers = new Map();
61
+ for (const signal of signals) {
62
+ const handler = () => { child.kill(signal); };
63
+ handlers.set(signal, handler);
64
+ process.on(signal, handler);
65
+ }
66
+ function cleanup() {
67
+ for (const [signal, handler] of handlers)
68
+ process.removeListener(signal, handler);
69
+ }
70
+ child.once('error', err => {
71
+ cleanup();
72
+ const code = values.hasErrorCode(err, 'ENOENT') ? 'COMMAND_NOT_FOUND' : 'COMMAND_START_FAILED';
73
+ process.stderr.write(`[resenv] ${code}: Could not start the requested command.\n`);
74
+ process.exitCode = values.hasErrorCode(err, 'ENOENT') ? 127 : 126;
75
+ });
76
+ child.once('exit', (code, signal) => {
77
+ cleanup();
78
+ if (signal) {
79
+ process.exitCode = 128 + (node_os.constants.signals[signal] || 1);
80
+ if (process.platform !== 'win32')
81
+ process.kill(process.pid, signal);
82
+ }
83
+ else {
84
+ process.exitCode = code ?? 1;
85
+ }
86
+ });
87
+ }
88
+ void main().catch(err => {
89
+ process.stderr.write(`${index.formatError(err)}\n`);
90
+ process.exitCode = 1;
91
+ });
package/bin/resenv.mjs ADDED
@@ -0,0 +1 @@
1
+ import './resenv.cjs';