@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.
- package/CONTRIBUTING.md +189 -0
- package/LICENSE +21 -0
- package/README.md +243 -0
- package/SECURITY.md +74 -0
- package/assert-safe-diagnostics.cjs +15 -0
- package/bin/resenv.cjs +91 -0
- package/bin/resenv.mjs +1 -0
- package/commands.cjs +85 -0
- package/docs/build.md +136 -0
- package/docs/dependencies.md +505 -0
- package/docs/doppler.md +146 -0
- package/docs/maintainer-handoff.md +112 -0
- package/docs/maturity.md +61 -0
- package/docs/public-readiness.md +138 -0
- package/docs/release-security.md +134 -0
- package/docs/releasing.md +143 -0
- package/docs/security-exceptions.json +139 -0
- package/docs/security-performance-review.md +150 -0
- package/docs/validation.md +700 -0
- package/doppler/auth.cjs +125 -0
- package/doppler/bridge.cjs +66 -0
- package/doppler/http.cjs +77 -0
- package/doppler/references.cjs +28 -0
- package/doppler/resolve.cjs +58 -0
- package/doppler/system.cjs +99 -0
- package/doppler-worker.cjs +28 -0
- package/doppler-worker.mjs +1 -0
- package/errors.cjs +12 -0
- package/index.cjs +88 -0
- package/index.d.cts +1 -0
- package/index.d.mts +1 -0
- package/index.mjs +1 -0
- package/package.json +89 -0
- package/preload.cjs +31 -0
- package/resolve-commands.cjs +5 -0
- package/resolve-commands.d.cts +1 -0
- package/resolve-commands.d.mts +1 -0
- package/resolve-commands.mjs +1 -0
- package/resolve-doppler.cjs +4 -0
- package/resolve-doppler.d.cts +1 -0
- package/resolve-doppler.d.mts +1 -0
- package/resolve-doppler.mjs +1 -0
- package/resolve.cjs +5 -0
- package/resolve.d.cts +1 -0
- package/resolve.d.mts +1 -0
- package/resolve.mjs +1 -0
- package/types/bin/resenv.d.ts +2 -0
- package/types/src/assert-safe-diagnostics.d.ts +1 -0
- package/types/src/commands.d.ts +4 -0
- package/types/src/doppler/auth.d.ts +19 -0
- package/types/src/doppler/bridge.d.ts +4 -0
- package/types/src/doppler/http.d.ts +9 -0
- package/types/src/doppler/references.d.ts +9 -0
- package/types/src/doppler/resolve.d.ts +9 -0
- package/types/src/doppler/system.d.ts +24 -0
- package/types/src/doppler-worker.d.ts +1 -0
- package/types/src/errors.d.ts +5 -0
- package/types/src/index.d.ts +7 -0
- package/types/src/preload.d.ts +4 -0
- package/types/src/resolve-commands.d.ts +1 -0
- package/types/src/resolve-doppler.d.ts +1 -0
- package/types/src/resolve.d.ts +1 -0
- package/types/src/types.d.ts +9 -0
- package/types/src/values.d.ts +5 -0
- package/values.cjs +20 -0
package/CONTRIBUTING.md
ADDED
|
@@ -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';
|