@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/commands.cjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_child_process = require('node:child_process');
|
|
4
|
+
var assertSafeDiagnostics = require('./assert-safe-diagnostics.cjs');
|
|
5
|
+
var errors = require('./errors.cjs');
|
|
6
|
+
var values = require('./values.cjs');
|
|
7
|
+
|
|
8
|
+
function resolveCommands(env, { commandTimeoutMs = 30000 }, debug) {
|
|
9
|
+
const resolved = values.copyEnvironment(env);
|
|
10
|
+
const entries = Object.entries(env).filter((entry) => typeof entry[1] === 'string' && entry[1].includes('$('));
|
|
11
|
+
if (!entries.length)
|
|
12
|
+
return resolved;
|
|
13
|
+
if (process.platform === 'win32') {
|
|
14
|
+
throw new errors.ResenvError('UNSUPPORTED_PLATFORM', 'Command resolution requires a POSIX /bin/sh shell.');
|
|
15
|
+
}
|
|
16
|
+
if (!Number.isSafeInteger(commandTimeoutMs) || commandTimeoutMs < 1 || commandTimeoutMs > 2147483647) {
|
|
17
|
+
throw new errors.ResenvError('INVALID_OPTIONS', 'commandTimeoutMs must be a positive 32-bit integer.');
|
|
18
|
+
}
|
|
19
|
+
// Validate every expression before starting any command. The shell parses the
|
|
20
|
+
// command body; resenv only supports an entire value wrapped in $(...).
|
|
21
|
+
for (const [name, value] of entries) {
|
|
22
|
+
if (!value.startsWith('$(') || !value.endsWith(')') || !value.slice(2, -1).trim()) {
|
|
23
|
+
debug('Failed to replace secret value for %s.', JSON.stringify(name));
|
|
24
|
+
throw new errors.ResenvError('INVALID_COMMAND_VALUE', `${JSON.stringify(name)} must contain a complete $(command) value. Embedded substitutions are not supported.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const [name, value] of entries) {
|
|
28
|
+
let result;
|
|
29
|
+
try {
|
|
30
|
+
assertSafeDiagnostics.assertSafeDiagnostics();
|
|
31
|
+
// Node supports detached sync children; its declaration omits the option.
|
|
32
|
+
const spawnOptions = {
|
|
33
|
+
env,
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
timeout: commandTimeoutMs,
|
|
36
|
+
killSignal: 'SIGKILL',
|
|
37
|
+
maxBuffer: 65536,
|
|
38
|
+
detached: true
|
|
39
|
+
};
|
|
40
|
+
result = node_child_process.spawnSync('/bin/sh', ['-c', value.slice(2, -1)], spawnOptions);
|
|
41
|
+
if (result.error || result.signal || result.status !== 0) {
|
|
42
|
+
// Each command has its own process group. Clean up surviving children
|
|
43
|
+
// after a timeout, output limit, signal or failed command.
|
|
44
|
+
if (result.pid) {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(-result.pid, 'SIGKILL');
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (!values.hasErrorCode(err, 'ESRCH')) {
|
|
50
|
+
throw new errors.ResenvError('COMMAND_CLEANUP_FAILED', 'Could not stop the command process group after a resolution failure.');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const code = values.hasErrorCode(result.error, 'ETIMEDOUT') ? 'COMMAND_TIMEOUT' : 'COMMAND_FAILED';
|
|
55
|
+
throw new errors.ResenvError(code, `Could not resolve ${JSON.stringify(name)}. Check the command and your secret-provider login.`);
|
|
56
|
+
}
|
|
57
|
+
let output;
|
|
58
|
+
try {
|
|
59
|
+
output = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(result.stdout);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new errors.ResenvError('INVALID_ENV_VALUE', `Command output for ${JSON.stringify(name)} is not valid UTF-8.`);
|
|
63
|
+
}
|
|
64
|
+
if (output.includes('\0')) {
|
|
65
|
+
throw new errors.ResenvError('INVALID_ENV_VALUE', `Command output for ${JSON.stringify(name)} contains a null byte.`);
|
|
66
|
+
}
|
|
67
|
+
// Match POSIX command substitution: remove trailing LF, preserve spaces.
|
|
68
|
+
resolved[name] = output.replace(/\n+$/, '');
|
|
69
|
+
debug('Replaced secret value for %s.', JSON.stringify(name));
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
debug('Failed to replace secret value for %s.', JSON.stringify(name));
|
|
73
|
+
if (err instanceof errors.ResenvError)
|
|
74
|
+
throw err;
|
|
75
|
+
throw new errors.ResenvError('COMMAND_FAILED', `Could not resolve ${JSON.stringify(name)}. Check the command and your secret-provider login.`);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
result?.stdout?.fill(0);
|
|
79
|
+
result?.stderr?.fill(0);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return resolved;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
exports.resolveCommands = resolveCommands;
|
package/docs/build.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Build and package contract
|
|
2
|
+
|
|
3
|
+
The repository is one Node.js package. The library and CLI are authored in
|
|
4
|
+
strict TypeScript. Vitest unit specs use colocated `*.spec.ts` files. Process,
|
|
5
|
+
package and integration checks and build orchestration use `.mjs`. Rollup's official
|
|
6
|
+
TypeScript plugin compiles the implementation; consumers need no TypeScript
|
|
7
|
+
loader or compiler.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm ci
|
|
11
|
+
npm run build
|
|
12
|
+
npm test
|
|
13
|
+
npm run test:package
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Output
|
|
17
|
+
|
|
18
|
+
| Public entry | ESM | CommonJS | Declarations |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| `@ciqol/resenv` | `index.mjs` | `index.cjs` | `index.d.mts`, `index.d.cts` |
|
|
21
|
+
| `@ciqol/resenv/resolve` | `resolve.mjs` | `resolve.cjs` | `resolve.d.mts`, `resolve.d.cts` |
|
|
22
|
+
| `@ciqol/resenv/resolve-commands` | `resolve-commands.mjs` | `resolve-commands.cjs` | `resolve-commands.d.mts`, `resolve-commands.d.cts` |
|
|
23
|
+
| `@ciqol/resenv/resolve-doppler` | `resolve-doppler.mjs` | `resolve-doppler.cjs` | `resolve-doppler.d.mts`, `resolve-doppler.d.cts` |
|
|
24
|
+
|
|
25
|
+
These paths are relative to `dist/`. The executable points to
|
|
26
|
+
`bin/resenv.cjs`; Rollup also emits its ESM wrapper. The command's user-facing
|
|
27
|
+
name stays `resenv`.
|
|
28
|
+
|
|
29
|
+
`resolve` reads the inherited environment and replaces Doppler references with
|
|
30
|
+
their fetched values before application code starts. Doppler is the only
|
|
31
|
+
built-in cloud provider. Original `encrypted:` values remain literal.
|
|
32
|
+
`resolve-doppler` imports that same preload, so loading both names does not resolve fetched output again. Arbitrary commands require the
|
|
33
|
+
separate `resolve-commands` entry or `run --commands`.
|
|
34
|
+
|
|
35
|
+
The private `doppler-worker.cjs` entry lets synchronous preloads wait for HTTPS
|
|
36
|
+
without turning the public CommonJS API into a promise. The worker is packed
|
|
37
|
+
alongside the entries but is not a public export. It is located through package
|
|
38
|
+
self-resolution, so the installed tarball does not depend on source paths.
|
|
39
|
+
|
|
40
|
+
The root exports map defines the public entries. The build compiles their
|
|
41
|
+
source with Rollup and its TypeScript plugin, then generates ESM facades from the exported
|
|
42
|
+
names. Both formats load one CommonJS implementation. This preserves error
|
|
43
|
+
class identity and shares the module cache when both preload forms are used.
|
|
44
|
+
It follows Node's documented mitigation for the
|
|
45
|
+
[dual-package hazard](https://nodejs.org/api/packages.html#dual-commonjs-es-module-packages).
|
|
46
|
+
|
|
47
|
+
The CommonJS build preserves the module graph. Source unit specs import their
|
|
48
|
+
adjacent modules; higher-level process and clean-consumer checks exercise the
|
|
49
|
+
compiled modules and the same archive. Matrix jobs do not recompile TypeScript. Only the
|
|
50
|
+
entries in the exports map are public package imports.
|
|
51
|
+
|
|
52
|
+
The package has no runtime npm dependencies. Node built-ins remain external;
|
|
53
|
+
Rollup does not bundle build tools into the application package. The build
|
|
54
|
+
resolves `import.meta.url` to the Node filename in CJS output; browser-like
|
|
55
|
+
globals in a Node test environment cannot change it.
|
|
56
|
+
[Rollup's external option](https://rollupjs.org/configuration-options/#external)
|
|
57
|
+
controls which imports remain outside the bundle.
|
|
58
|
+
|
|
59
|
+
The compiler emits declarations into `dist/types/`. Generated `.d.mts` and
|
|
60
|
+
`.d.cts` entry facades re-export that shared declaration graph; there is no
|
|
61
|
+
handwritten public declaration file or separate declaration-bundler dependency.
|
|
62
|
+
The generated package has no `type` field, so its shared `.d.ts` graph describes
|
|
63
|
+
the CommonJS implementation. The exports map lists `types` first and selects
|
|
64
|
+
the entry facade for the consumer's
|
|
65
|
+
module format, as described by
|
|
66
|
+
[TypeScript's ESM/CommonJS package guidance](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html).
|
|
67
|
+
Consumer compilation checks both formats, including invalid inputs that must
|
|
68
|
+
remain type errors.
|
|
69
|
+
|
|
70
|
+
Public options use type aliases and readonly properties. `Environment` stays
|
|
71
|
+
mutable for optional in-place replacement. `resolve()` always returns a new
|
|
72
|
+
`Environment`, with input replacement enabled only by `{ replace: true }`. It
|
|
73
|
+
does not preserve a generic input type: resolved strings cannot retain a
|
|
74
|
+
literal reference type. This intentionally tightens the unpublished API.
|
|
75
|
+
ESLint uses type information and enforces the selected style rules. The compiler
|
|
76
|
+
checks all library/CLI source, colocated unit specs and both consumer formats with `strict` and
|
|
77
|
+
`noUncheckedIndexedAccess`. Provider responses and parsed worker input start as
|
|
78
|
+
`unknown` and are validated before use. Compiler failures stop the build.
|
|
79
|
+
|
|
80
|
+
The source target is ES2023. `ES2024.String` supplies the declaration for
|
|
81
|
+
`isWellFormed()`, whose availability is also checked by the Node 20/22/24 runtime
|
|
82
|
+
tests. Node types match the Node 24 development toolchain; the runtime matrix
|
|
83
|
+
remains necessary to detect use of APIs absent from the oldest supported Node.
|
|
84
|
+
|
|
85
|
+
## Generated manifest and packing
|
|
86
|
+
|
|
87
|
+
The build removes stale output first. It derives `dist/package.json` from the
|
|
88
|
+
root manifest, rebases entry paths, and omits development dependencies, scripts,
|
|
89
|
+
overrides and a package-wide `type` field. Explicit `.mjs` and `.cjs` extensions
|
|
90
|
+
select the output format. README, the MIT `LICENSE`, contributor/security guides
|
|
91
|
+
and docs are copied into the distribution and included in the archive.
|
|
92
|
+
|
|
93
|
+
Ordinary local builds retain `private: true`. The CI build accepts a validated
|
|
94
|
+
release plan and sets its computed version, removing `private` before the final
|
|
95
|
+
archive is packed and tested. The development root stays private. Earlier
|
|
96
|
+
builds kept both manifests private while the package was an unpublished
|
|
97
|
+
prototype. There is no publishing workflow.
|
|
98
|
+
|
|
99
|
+
`npm run pack:local` packs the generated directory. `npm run test:package`
|
|
100
|
+
instead packs into a temporary directory and installs it offline into a clean
|
|
101
|
+
consumer. It checks imports, requires, preloads, CLI behavior and declarations;
|
|
102
|
+
the consumer cannot load resenv's source files or build dependencies. The
|
|
103
|
+
runtime entries from the repository lockfile are used in the consumer lockfile.
|
|
104
|
+
Its only direct dependency is the packed archive. An offline `npm ci` verifies
|
|
105
|
+
archive integrity and installs the reviewed versions without registry metadata.
|
|
106
|
+
|
|
107
|
+
With `npm run test:package -- /path/to/package.tgz`, the same checks inspect
|
|
108
|
+
and install the supplied archive without rebuilding it. Its name, version,
|
|
109
|
+
manifest and executable mode must match the package contract. CI uses this
|
|
110
|
+
form so every platform checks the same built bytes.
|
|
111
|
+
|
|
112
|
+
Keep tarballs already consumed by other projects immutable. No existing
|
|
113
|
+
consumer artifact is rebuilt in place by these tests.
|
|
114
|
+
|
|
115
|
+
## CI
|
|
116
|
+
|
|
117
|
+
The `build` job uses normal npm lifecycle behavior, checks dependency advisories and
|
|
118
|
+
signatures, runs typed lint, implementation/spec checks and Vitest source coverage,
|
|
119
|
+
builds once and uploads the packed
|
|
120
|
+
archive. The `test` matrix downloads that artifact, verifies the installed
|
|
121
|
+
package, extracts the same build
|
|
122
|
+
for the process tests, and never recompiles it. The matrix checks Node 20
|
|
123
|
+
and 22 on Linux plus the `.nvmrc` version on Linux and macOS.
|
|
124
|
+
|
|
125
|
+
Jobs have read-only repository permissions and receive no application or
|
|
126
|
+
registry secrets. Missing artifacts and digest mismatches fail the run. The
|
|
127
|
+
archive is retained for seven days for maintainer review. The workflow has no
|
|
128
|
+
publish or deploy step. Hosted results must be observed before treating the
|
|
129
|
+
workflow definition as a passing CI run.
|
|
130
|
+
|
|
131
|
+
Checkout and setup/download actions use full commit pins. The GitHub-owned
|
|
132
|
+
upload action uses the Tashtit exception for a complete release tag; major tags
|
|
133
|
+
and third-party movable refs are not permitted. Installs use normal npm lifecycle
|
|
134
|
+
behavior, and dependency updates require an install-hook review. Build commands
|
|
135
|
+
run explicitly.
|
|
136
|
+
See the [maturity evaluation](maturity.md) for the static catalog results.
|