@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
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.