@ciqol/resenv 0.1.0-beta.1 → 0.1.0

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.
@@ -3,96 +3,47 @@
3
3
  var node_child_process = require('node:child_process');
4
4
  var node_fs = require('node:fs');
5
5
  var node_os = require('node:os');
6
+ var node_path = require('node:path');
6
7
  var errors = require('../errors.cjs');
7
8
  var assertSafeDiagnostics = require('../assert-safe-diagnostics.cjs');
8
9
  var http = require('./http.cjs');
9
10
 
10
- // Preserve saved logins from before the package rename.
11
- const SERVICE = 'ciqol.envref.doppler';
12
- const ACCOUNT = 'default';
13
- // Do not pass application secrets, NODE_OPTIONS, loader variables or a
14
- // repository's PATH to credential helpers. No helper is invoked via a shell.
15
- function helperEnvironment() {
16
- const env = { HOME: node_os.userInfo().homedir, PATH: '/usr/bin:/bin', LANG: 'C.UTF-8' };
17
- for (const key of ['SystemRoot', 'USERPROFILE', 'DBUS_SESSION_BUS_ADDRESS', 'XDG_RUNTIME_DIR', 'DISPLAY', 'WAYLAND_DISPLAY']) {
11
+ // Do not pass application secrets, loader variables or a repository's PATH to
12
+ // the worker or CLI. Keep only the OS session needed by Doppler's own storage.
13
+ function helperEnvironment({ platform = process.platform, home = node_os.userInfo().homedir } = {}) {
14
+ const env = { HOME: home, PATH: '/usr/bin:/bin', LANG: 'C.UTF-8' };
15
+ for (const key of ['SystemRoot', 'DBUS_SESSION_BUS_ADDRESS', 'XDG_RUNTIME_DIR', 'DISPLAY', 'WAYLAND_DISPLAY']) {
18
16
  if (process.env[key])
19
17
  env[key] = process.env[key];
20
18
  }
19
+ if (platform === 'win32') {
20
+ env.USERPROFILE = home;
21
+ env.APPDATA = node_path.win32.join(home, 'AppData', 'Roaming');
22
+ env.LOCALAPPDATA = node_path.win32.join(home, 'AppData', 'Local');
23
+ env.PATH = node_path.win32.join(process.env.SystemRoot || 'C:\\Windows', 'System32');
24
+ }
21
25
  return env;
22
26
  }
23
- function createSystem({ platform = process.platform, run = node_child_process.spawnSync, exists = node_fs.existsSync } = {}) {
24
- const invoke = (file, args, options = {}) => {
25
- assertSafeDiagnostics.assertSafeDiagnostics();
26
- return run(file, args, {
27
- env: helperEnvironment(), encoding: 'utf8', timeout: 15000,
28
- maxBuffer: 16384, stdio: ['pipe', 'pipe', 'pipe'], shell: false, ...options
29
- });
30
- };
31
- const mac = platform === 'darwin' && exists('/usr/bin/security');
32
- const linux = platform === 'linux' && exists('/usr/bin/secret-tool');
33
- const cli = ['/opt/homebrew/bin/doppler', '/usr/local/bin/doppler', '/usr/bin/doppler'].find(exists);
34
- const unavailable = () => new errors.ResenvError('DOPPLER_KEYRING_UNAVAILABLE', 'The OS credential store is unavailable. Use the Doppler CLI or supply DOPPLER_TOKEN.');
35
- function read() {
36
- if (!mac && !linux)
37
- return undefined;
38
- const result = mac
39
- ? invoke('/usr/bin/security', ['find-generic-password', '-s', SERVICE, '-a', ACCOUNT, '-w'])
40
- : invoke('/usr/bin/secret-tool', ['lookup', 'service', SERVICE, 'account', ACCOUNT]);
41
- if (!result.error && result.status === 0)
42
- return http.validateToken(result.stdout.trim());
43
- if (!result.error && ((mac && result.status === 44) || (linux && result.status === 1 && !result.stderr)))
44
- return undefined;
45
- throw unavailable();
46
- }
47
- function write(token) {
48
- http.validateToken(token);
49
- if (!mac && !linux)
50
- throw unavailable();
51
- // security's interactive interpreter receives the token on stdin, never
52
- // the OS process argument list. The token alphabet excludes separators.
53
- const result = mac
54
- ? invoke('/usr/bin/security', ['-i'], { input: `add-generic-password -U -s ${SERVICE} -a ${ACCOUNT} -w ${token}\n` })
55
- : invoke('/usr/bin/secret-tool', ['store', '--label=resenv Doppler', 'service', SERVICE, 'account', ACCOUNT], { input: token });
56
- if (result.error || result.status !== 0)
57
- throw unavailable();
58
- // security -i may report a subcommand failure with a zero process status.
59
- if (read() !== token)
60
- throw unavailable();
61
- }
62
- function remove() {
63
- if (!mac && !linux)
64
- throw unavailable();
65
- const result = mac
66
- ? invoke('/usr/bin/security', ['delete-generic-password', '-s', SERVICE, '-a', ACCOUNT])
67
- : invoke('/usr/bin/secret-tool', ['clear', 'service', SERVICE, 'account', ACCOUNT]);
68
- if (result.error || (result.status !== 0 && !(mac && result.status === 44)))
69
- throw unavailable();
70
- }
27
+ function createSystem({ platform = process.platform, home = node_os.userInfo().homedir, run = node_child_process.spawnSync, exists = node_fs.existsSync } = {}) {
28
+ // Use standard installation locations so npm's repository-local PATH cannot
29
+ // substitute a credential helper. Windows locations cover WinGet and Scoop.
30
+ const paths = platform === 'win32'
31
+ ? [node_path.win32.join(home, 'AppData', 'Local', 'Microsoft', 'WinGet', 'Links', 'doppler.exe'), node_path.win32.join(home, 'scoop', 'apps', 'doppler', 'current', 'doppler.exe')]
32
+ : ['/opt/homebrew/bin/doppler', '/usr/local/bin/doppler', '/usr/bin/doppler', node_path.join(home, '.local', 'bin', 'doppler'), node_path.join(home, 'bin', 'doppler')];
71
33
  function cliToken() {
34
+ assertSafeDiagnostics.assertSafeDiagnostics();
35
+ const cli = paths.find(exists);
72
36
  if (!cli)
73
- return undefined;
74
- const result = invoke(cli, ['configure', 'get', 'token', '--plain']);
37
+ throw new errors.ResenvError('DOPPLER_CLI_UNAVAILABLE', 'Install the Doppler CLI in a standard location and run doppler login, or supply DOPPLER_TOKEN. See https://docs.doppler.com/docs/cli.');
38
+ const result = run(cli, ['configure', 'get', 'token', '--plain', '--no-check-version', '--no-read-env'], {
39
+ env: helperEnvironment({ platform, home }), encoding: 'utf8', timeout: 15000,
40
+ maxBuffer: 16384, stdio: ['pipe', 'pipe', 'pipe'], shell: false
41
+ });
75
42
  if (result.error || result.status !== 0 || !result.stdout.trim())
76
43
  return undefined;
77
44
  return http.validateToken(result.stdout.trim());
78
45
  }
79
- function cliLogin() {
80
- if (!cli)
81
- throw new errors.ResenvError('DOPPLER_CLI_UNAVAILABLE', 'Doppler CLI fallback is unavailable. Install Doppler or supply DOPPLER_TOKEN.');
82
- const result = invoke(cli, ['login'], { stdio: 'inherit', timeout: 300000 });
83
- if (result.error || result.status !== 0)
84
- throw new errors.ResenvError('DOPPLER_LOGIN_FAILED', 'Doppler CLI login did not complete.');
85
- if (!cliToken())
86
- throw new errors.ResenvError('DOPPLER_LOGIN_FAILED', 'Doppler CLI login did not provide a credential.');
87
- }
88
- function openBrowser(url) {
89
- const file = mac ? '/usr/bin/open' : '/usr/bin/xdg-open';
90
- if (!exists(file))
91
- return false;
92
- const result = invoke(file, [url]);
93
- return !result.error && result.status === 0;
94
- }
95
- return { read, write, remove, cliToken, cliLogin, openBrowser, canStore: mac || linux };
46
+ return { cliToken };
96
47
  }
97
48
 
98
49
  exports.createSystem = createSystem;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@ciqol/resenv",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0",
4
4
  "description": "Replace environment references with their resolved values at application startup",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public",
8
- "tag": "beta",
8
+ "tag": "latest",
9
9
  "registry": "https://registry.npmjs.org/"
10
10
  },
11
11
  "repository": {
@@ -1,19 +1,4 @@
1
1
  import type { Environment } from '../types.js';
2
- import type { Request } from './http.js';
3
2
  import type { CredentialSystem } from './system.js';
4
- type LoginOptions = {
5
- readonly cli?: boolean;
6
- readonly noBrowser?: boolean;
7
- };
8
- type LoginDependencies = {
9
- readonly request?: Request;
10
- readonly system?: CredentialSystem;
11
- readonly output?: (text: string) => void;
12
- readonly interactive?: boolean;
13
- readonly env?: Readonly<Environment>;
14
- readonly sleep?: (ms: number) => Promise<unknown>;
15
- readonly now?: () => number;
16
- };
17
- declare function credential(env: Readonly<Environment>, system?: Pick<CredentialSystem, 'read' | 'cliToken'>): Promise<string>;
18
- declare function login({ cli, noBrowser }?: LoginOptions, { request, system, output, interactive, env, sleep, now }?: LoginDependencies): Promise<'cli' | 'native'>;
19
- export { credential, login };
3
+ declare function credential(env: Readonly<Environment>, system?: CredentialSystem): Promise<string>;
4
+ export { credential };
@@ -1,9 +1,7 @@
1
1
  export type RequestOptions = {
2
2
  readonly token?: string;
3
- readonly body?: Readonly<Record<string, string>>;
4
- readonly pending?: boolean;
5
3
  };
6
- export type Request = (path: string, options?: RequestOptions) => Promise<Record<string, unknown> | null>;
4
+ export type Request = (path: string, options?: RequestOptions) => Promise<Record<string, unknown>>;
7
5
  declare function createClient(fetchImpl?: typeof globalThis.fetch): Request;
8
6
  declare function validateToken(token: unknown): string;
9
7
  export { createClient, validateToken };
@@ -7,18 +7,13 @@ type SyncOptions = SpawnSyncOptionsWithStringEncoding & {
7
7
  export type SyncRunner = (file: string, args: readonly string[], options: SyncOptions) => SpawnSyncReturns<string>;
8
8
  type SystemOptions = {
9
9
  readonly platform?: NodeJS.Platform;
10
+ readonly home?: string;
10
11
  readonly run?: SyncRunner;
11
12
  readonly exists?: typeof existsSync;
12
13
  };
13
14
  export type CredentialSystem = ReturnType<typeof createSystem>;
14
- declare function helperEnvironment(): Environment;
15
- declare function createSystem({ platform, run, exists }?: SystemOptions): {
16
- read: () => string | undefined;
17
- write: (token: string) => void;
18
- remove: () => void;
15
+ declare function helperEnvironment({ platform, home }?: Pick<SystemOptions, 'platform' | 'home'>): Environment;
16
+ declare function createSystem({ platform, home, run, exists }?: SystemOptions): {
19
17
  cliToken: () => string | undefined;
20
- cliLogin: () => void;
21
- openBrowser: (url: string) => boolean;
22
- canStore: boolean;
23
18
  };
24
19
  export { createSystem, helperEnvironment };
package/docs/maturity.md DELETED
@@ -1,61 +0,0 @@
1
- # Maturity evaluation
2
-
3
- Evaluated on October 8, 2026 with the Tashtit maturity 0.3.1 catalog, using
4
- static working-tree evidence only. This measures the catalog's repository
5
- hygiene rules; it does not certify security or readiness to publish. Tests
6
- and hosted checks are recorded separately in [validation](validation.md).
7
-
8
- ## Maturity: 91% (11/120 weight failing)
9
-
10
- The maintained source tree scores 100% (0/97 weight failing). The full catalog
11
- also discovers the generated `dist/package.json`, so its findings are retained.
12
-
13
- | Scope | Failing weight | Relevant weight |
14
- | --- | ---: | ---: |
15
- | Root npm project | 0 | 41 |
16
- | Generated distribution | 11 | 23 |
17
- | Repository hygiene | 0 | 24 |
18
- | GitHub Actions | 0 | 32 |
19
- | Total | 11 | 120 |
20
-
21
- ## Findings
22
-
23
- - **High: generated package has no sibling lockfile** (`lockfile-missing`, 8).
24
- Evidence: `dist/package.json:2`; no `dist/package-lock.json` exists. A maintained
25
- npm project needs a lockfile. This directory is generated from the root's v3
26
- lockfile and consumers maintain their own locks; it is not another workspace.
27
- Reference: [npm lockfiles](https://docs.npmjs.com/cli/configuring-npm/package-lock-json).
28
- - **Medium: generated package has no sibling runtime-version file**
29
- (`setup-nvmrc`, 3). Evidence: `dist/package.json:2`; no `dist/.nvmrc` exists.
30
- Development projects should pin a runtime. The generated package declares
31
- `engines.node: >=20` and does not need contributor setup files. The root
32
- `.nvmrc` pins Node 24.19.0. Reference: [nvm version files](https://github.com/nvm-sh/nvm#nvmrc).
33
-
34
- ## Passed checks
35
-
36
- - Root: one v3 npm lockfile, no competing yarn lockfile, `.npmrc`, `.nvmrc`, and
37
- registry dependencies with exact direct versions. CI uses `npm ci` and
38
- normal lifecycle behavior; it does not pass `--ignore-scripts`.
39
- - Repository: valid CODEOWNERS syntax, README, CONTRIBUTING and `.editorconfig`.
40
- - CI: a parseable workflow, explicit scoped token permissions, bounded job
41
- timeouts, no `pull_request_target`, and pinned remote actions. The GitHub-owned
42
- upload action's exact `v7.0.1` tag is permitted by the catalog.
43
-
44
- The earlier 88% score included a lifecycle-script finding that is now resolved.
45
- No dependency installation, build, test, or remote request was used to derive
46
- this static score.
47
-
48
- ## Not evaluated by this catalog
49
-
50
- - Docker rules: no candidate Dockerfile exists.
51
- - Repository protections, private vulnerability reporting and maintainer access.
52
- - License suitability, secret history, provider login, and runtime behavior.
53
-
54
- See [public readiness](public-readiness.md) for the separate exposure audit and
55
- [validation](validation.md) for observed checks.
56
-
57
- ## Fixable findings
58
-
59
- No automatic fix is appropriate. The generated package's broad runtime range
60
- does not identify a development version to pin, and adding another lockfile
61
- would misrepresent it as a maintained workspace.
@@ -1,138 +0,0 @@
1
- # Public repository readiness
2
-
3
- ## Follow-up review — 2026-10-12
4
-
5
- The repository is now public, with `beta` as the sole remote and default branch.
6
- CI has staged the first `0.1.0-beta.1` candidate; npm approval and release
7
- recording are still pending. The accidental public `1.0.0` has been removed,
8
- and the `0.0.0-stage` placeholder remains. Recheck live state before release work.
9
-
10
- Private vulnerability reporting returned `enabled: false` during this review.
11
- The security policy's reporting prerequisite remains outstanding until that
12
- setting is enabled and verified. See [the session handoff](maintainer-handoff.md)
13
- and [validation](validation.md) for current behavior and observed checks.
14
- The dated transition plan below is historical evidence.
15
-
16
- ## Approved transition - 2026-10-10
17
-
18
- The current release pipeline and launch state are documented in [releasing](releasing.md).
19
- The release-tool review and expiring advisory policy are in [release security](release-security.md).
20
- Earlier dated entries below remain historical evidence and are superseded where
21
- the current release guide describes a different state.
22
-
23
- The user approved public visibility, default branch `beta`, and deletion of
24
- the four older ancestor branch references. A fresh settings snapshot, complete
25
- Git bundle and verified restore are retained outside the repository. Recheck
26
- remote heads and ancestry immediately before deletion. Other governance changes
27
- and PR creation remain outside this transition.
28
-
29
-
30
- Reviewed on October 8, 2026 at `cb6ebe92c3182051a034dd29e192d51a182b5d22`, before
31
- the accompanying documentation cleanup. Repository: `ciqol/resenv`. Maintainer
32
- and authenticated GitHub account: `titan-ron`.
33
-
34
- ## Result
35
-
36
- No real credentials or private machine/employer references were identified in
37
- the reviewed Git history or hosted Actions outputs. The source can become a
38
- public experimental repository after the remaining decisions below. This does
39
- not establish npm release readiness or an independent security audit.
40
-
41
- The repository remains private. No branch, default-branch, access, protection,
42
- or visibility setting was changed by this audit.
43
-
44
- ## Exposure review
45
-
46
- - Gitleaks 8.30.1 scanned all 24 commits reachable from the fetched refs. Its
47
- nine matches were synthetic Doppler credentials in tests: eight use a dummy
48
- marker and one uses an alternative fixture marker with sequential digits.
49
- The tests inject provider and credential helpers instead of authenticating
50
- those values. No detector exception or history rewrite was added.
51
- - TruffleHog 3.97.4 scanned the local Git history with network verification and
52
- update checks disabled. It reported no credentials. Sandbox process-inspection
53
- restrictions produced temporary-cleanup warnings; the scan completed.
54
- - Reachable-blob inspection found no committed dotenv/private-key files,
55
- absolute developer paths, employer references, or private service URLs.
56
- - All 14 existing Actions log archives and 14 unexpired package artifacts were
57
- downloaded into a private audit folder and scanned offline. Gitleaks found
58
- no secrets. Package inspection covered 755 regular files across the archives
59
- and found no credential files or private developer/employer references.
60
- - There are no existing issues or pull requests to review for public exposure.
61
- - The latest pre-cleanup [CI run](https://github.com/ciqol/resenv/actions/runs/37778134741)
62
- passed on Linux Node 20/22/24 and macOS Node 24.
63
-
64
- Detection tools cannot prove the absence of every secret. No candidate
65
- credential was sent to a provider to test whether it works. Git history remains
66
- visible after deleting an ancestor branch. GitHub exposes existing Actions
67
- history and logs when visibility becomes public; see
68
- [GitHub's visibility documentation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/setting-repository-visibility).
69
-
70
- ## Cleanup prepared
71
-
72
- - Ignore local `.idea/` settings without deleting the developer's files.
73
- - Keep installation and contributor text independent of repository visibility.
74
- - Include the MIT license and ciqol contributors copyright, selected on October 8.
75
- Registry publication remains disabled.
76
- - Mark historical validation/dependency entries so old APIs and version plans
77
- are not mistaken for current usage instructions.
78
- - Remove unnecessary references to other project names from current docs.
79
- Historical commits remain unchanged.
80
- - Direct security questions from the issue chooser to the security policy.
81
- - Prepare `beta` as the sole normal release branch on npm's `beta` channel. CI
82
- also checks the current `titan/beta` preparation branch until branch cleanup.
83
- PR checks and manual dispatch remain; publishing stays disabled.
84
- - Recalculate maturity: 91% including generated output, 100% for the maintained
85
- source tree. These scores do not establish release readiness.
86
-
87
- ## Remaining decisions and settings
88
-
89
- 1. **License resolved:** MIT was selected on October 8, 2026, with copyright
90
- held by ciqol contributors. The root and generated manifests declare MIT;
91
- the [license](../LICENSE) is included in the package archive.
92
- 2. **Branches:** the selected sole normal release branch is `beta`, with npm
93
- channel `beta`. A local dry run with only that branch calculated `1.0.0`;
94
- another stable branch is unnecessary. Default-branch selection and deletion
95
- of the older branches remain pending. See [release preparation](releasing.md).
96
- 3. **Private reporting:** the endpoint returned HTTP 404 while the repository
97
- is private. Enable and verify it after the approved visibility transition,
98
- and verify the reporting option described in SECURITY.md. It is not yet available.
99
- 4. **Protections:** all branch entries report `protected: false`; the ruleset
100
- API returned HTTP 403 under the current private plan. Re-audit effective
101
- protections after the transition. No protection policy was applied.
102
-
103
- ## Proposed transition
104
-
105
- This plan is for review. It does not authorize npm publication, collaborator or
106
- team changes, merge-policy changes, rulesets, or a pull request.
107
-
108
- | Branch | Observed head | Action for the beta-only layout |
109
- | --- | --- | --- |
110
- | `beta` | Same reviewed source head as the preparation branch | Keep; make default |
111
- | `titan/beta` | Current preparation branch | Delete after verifying its head is retained by `beta` |
112
- | `titan/bootstrap-envref` | `b25da7cfe730bfaca0a470d9a31a303cef7d33ad` | Delete branch pointer |
113
- | `titan/rename-resenv` | `60e70a4e43b39fc9a87379990e4ed44de50726cb` | Delete branch pointer |
114
- | `titan/prepare-first-release` | `0c313483d7123b8b4afd360b4e8daae58abc4241` | Delete after changing the default |
115
-
116
- At the original audit, all three older heads were ancestors of `titan/beta`;
117
- none contained unique commits. Before deletion, re-check every remote head and
118
- verify all four older branches are ancestors of the retained `beta` head. Stop
119
- if any contains newer or unique work.
120
-
121
- After the branch decision, obtain explicit approval for the default
122
- branch, listed deletions, public visibility, and private vulnerability reporting.
123
- The CI push filter and release docs are prepared for this layout. Apply
124
- only approved scopes and verify branches, default, visibility, reporting and
125
- CI by read-back. Keep npm publishing disabled until its separate prerequisites
126
- are complete.
127
-
128
- ## Recovery
129
-
130
- A private settings snapshot and complete Git bundle are retained outside this
131
- repository. Bundle verification and a restore into a separate local bare
132
- repository reproduced all four original remote branch heads exactly.
133
-
134
- Restore deleted branch pointers from their recorded SHAs or bundle before
135
- restoring the old default branch. Visibility can be set back to private, but
136
- public forks, downloaded code and copied logs cannot be recalled. A settings
137
- rollback cannot undo public disclosure. Never overwrite newer branch work or
138
- weaken protections to force a restore.
@@ -1,150 +0,0 @@
1
- # Security and performance review
2
-
3
- Historical review: the October 12 beta removes dotenvx decryption and its entire
4
- runtime dependency tree. Earlier cryptography and ciphertext findings below
5
- apply to the prototype. Current behavior is in [Security](../SECURITY.md).
6
-
7
- ## Release preparation update - 2026-10-10
8
-
9
- The current release pipeline and launch state are documented in [releasing](releasing.md).
10
- The release-tool review and expiring advisory policy are in [release security](release-security.md).
11
- Earlier dated entries below remain historical evidence and are superseded where
12
- the current release guide describes a different state.
13
-
14
-
15
- Reviewed on 2026-10-06 against the private 0.1.0 candidate, starting at
16
- `b50fa9b`. This is a source review with targeted reproductions, regression tests
17
- and measurements. It is not an independent audit or a security certification.
18
-
19
- A later change on the same day migrates the implementation and CLI to strict
20
- TypeScript. The findings below describe the reviewed baseline and fixes; see
21
- [the current build contract](build.md) and [validation](validation.md) for the
22
- generated declarations and implementation checks that supersede the earlier
23
- declaration-only setup.
24
-
25
- ## Findings and changes
26
-
27
- | Priority | Finding reproduced | Change and regression coverage |
28
- | --- | --- | --- |
29
- | P1 | Node's `child_process` debug category printed dummy credentials from subprocess environment values and private worker stdin. | Reject resenv subprocess/helper operations before spawning when that native debug category is enabled. Cover wildcards, case, cached debug state and safe no-work preloads in [diagnostic tests](https://github.com/ciqol/resenv/blob/e57a8fe72ecf956fc99c661d40b1ca707bbcd172/test/diagnostics.test.mjs). |
30
- | P1 | Combining automatic resolution modes could interpret a fetched secret as a shell expression on the second pass. | Share preload state across CJS/ESM, reject mixed modes and reject CLI resolution after a preload. Verify both preload orders and the CLI combination in [runtime tests](https://github.com/ciqol/resenv/blob/e57a8fe72ecf956fc99c661d40b1ca707bbcd172/test/doppler-runtime.test.mjs). |
31
- | P2 | Invalid UTF-8 from the provider was silently converted to a different secret value; escaped lone surrogates were also accepted. | Decode JSON with fatal UTF-8 validation and reject malformed Unicode values. Cover both paths in [Doppler tests](https://github.com/ciqol/resenv/blob/e57a8fe72ecf956fc99c661d40b1ca707bbcd172/test/doppler.test.mjs). |
32
- | P2 | A non-writable second property could fail after the first environment property had already changed. | Preflight all changed descriptors before committing; reject setters and Proxy targets. Preserve writable sealed records. Cover atomicity in [resolver tests](https://github.com/ciqol/resenv/blob/e57a8fe72ecf956fc99c661d40b1ca707bbcd172/test/resolver.test.mjs). |
33
- | P2 | Unique Doppler lookups ran sequentially, making startup latency grow with each request. | Deduplicate references and allow four requests at a time. Stop scheduling after failure, await active requests and discard partial results. Deterministic tests check overlap, deduplication and failure behavior. |
34
- | P2 | The generic in-place return type preserved an input's literal reference type after its value had changed. | Return `Environment`; add compile-time regression checks to both installed CJS and ESM consumers. |
35
-
36
- Unexpected cleanup errors now produce safe errors instead of disappearing in
37
- empty catch blocks. Known already-exited process groups remain harmless.
38
- Credential-revocation failures retain a safe message without raw provider
39
- details. Regression tests cover these cases with dummy values.
40
-
41
- The debug guard uses Node's actual `debuglog().enabled` state. Clearing
42
- `NODE_DEBUG` after Node initializes it is insufficient. Use `NODE_DEBUG=ciqol:resenv`
43
- for variable names and outcome messages without values. See Node's
44
- [debuglog documentation](https://nodejs.org/api/util.html#utildebuglogsection-callback).
45
-
46
- ## Reviewed boundaries
47
-
48
- - Only original environment references are processed within a resolution call.
49
- Fetched and decrypted output remains literal.
50
- - Doppler requests use a fixed HTTPS origin, reject redirects and request only
51
- named secrets. An explicit identity is not silently replaced after rejection.
52
- - The synchronous worker receives references and an optional token over stdin.
53
- Credential helpers receive a filtered environment and fixed executable paths.
54
- Application secrets, loader options and a repository's PATH are not forwarded
55
- to those helpers.
56
- - Requests have a ten-second deadline and a 1 MiB response limit. A resolution
57
- allows at most 128 references, with a 64 KiB limit per cloud value. The worker
58
- has a 60-second deadline and 1 MiB captured-output limit.
59
- - Shell execution requires explicit opt-in. Startup never starts interactive
60
- login automatically; login rejects CI and noninteractive use.
61
- - Plain environments avoid subprocesses, credential discovery and loading the
62
- cryptography dependency. Runtime dependency versions are unchanged.
63
-
64
- These boundaries do not isolate secrets from the application, a debugger or
65
- other code running as the same user. Commands execute with that user's access,
66
- and their external effects cannot be rolled back. Configure one resolver per
67
- launch; repeated manual calls or a separately installed child-process preload
68
- can reinterpret values. See [SECURITY.md](../SECURITY.md).
69
-
70
- ## Performance measurements
71
-
72
- Run `npm run bench` after building. The benchmark uses dummy values and a mocked
73
- provider, with no real credentials or network requests. Measurements below use
74
- Node 24.19.0 on macOS arm64; they are observations, not CI timing thresholds.
75
-
76
- | Measurement | Before | After |
77
- | --- | ---: | ---: |
78
- | 16 references, eight unique requests, 40 ms simulated latency per request; median of five runs | 332.069 ms | 81.705 ms |
79
- | Peak concurrent provider requests | 1 | 4 |
80
- | No-work library call with 200 plain variables; median of 5,000 calls | 0.047 ms | 0.045 ms |
81
-
82
- The mocked lookup case is about 4.1 times faster. Real Doppler latency, rate
83
- limits and credential-store latency may differ; four is a conservative client
84
- limit, not a published provider concurrency allowance.
85
-
86
- The final fresh-process measurements interleave baseline, CJS preload and ESM
87
- preload runs. Each reports the median of 20 samples after discarding three
88
- warmup samples; disk caches remain warm.
89
-
90
- | Final startup measurement | Median |
91
- | --- | ---: |
92
- | Node alone | 46.427 ms |
93
- | Node with `--require @ciqol/resenv/resolve` and no references | 52.177 ms |
94
- | Node with `--import @ciqol/resenv/resolve` and no references | 50.722 ms |
95
-
96
- The observed extra startup cost was about 4-6 ms. Scheduling and filesystem
97
- noise affect these small measurements; no startup speedup is claimed.
98
-
99
- ## Standards applied
100
-
101
- The requested TypeScript authoring, library-build and style standards were
102
- applied to the existing `.mjs` project and its public declarations:
103
-
104
- - Public option objects use type aliases and readonly properties. `Environment`
105
- remains mutable because in-place resolution writes it.
106
- - ESLint checks runtime/build JavaScript and declarations. Rules cover undefined
107
- names, empty handlers, parameter count, catch naming and the selected public
108
- TypeScript conventions. Existing compact JavaScript formatting is preserved.
109
- - Declaration checks and installed `.cts`/`.mts` consumers use `strict` and
110
- `noUncheckedIndexedAccess`. This checks the declaration contract, not the
111
- `.mjs` implementation with TypeScript.
112
- - Rollup still emits explicit CJS/ESM extensions with a shared implementation,
113
- types-first conditional exports and external runtime dependencies. Clean
114
- consumers verify the packed artifact without source-tree links or lint/build
115
- tools installed as runtime dependencies.
116
- - CI runs lint and declaration checks before building one archive for every
117
- compatibility job. Actions are pinned, permissions are read-only and install
118
- lifecycle scripts are disabled. Both manifests remain private by request.
119
-
120
- The new development dependencies and their provenance limits are recorded in
121
- [the dependency review](dependencies.md). No new runtime dependency was added.
122
-
123
- ## Validation and remaining work
124
-
125
- - All 70 unit/process tests passed locally on Node 20.20.2, 22.22.3 and 24.19.0.
126
- - The packed package passed 13 runtime checks and two typed consumers on Node
127
- 24. Lint and strict declaration checks passed.
128
- - Nx 23.2.1 passed target dotenv loading, local/shell precedence, target
129
- isolation and strict failures for command and Doppler modes. Tests used a
130
- temporary workspace, dummy credentials and an existing Nx installation
131
- read-only; consumer files were not changed.
132
- - npm reported zero known vulnerabilities, 103 verified registry signatures
133
- and 37 verified attestations in the installed development tree. Advisory and
134
- signature checks do not rule out unknown vulnerabilities.
135
- - [Hosted CI](https://github.com/ciqol/resenv/actions/runs/37462966577) passed for
136
- `e57a8fe`: one build and four compatibility jobs on Linux (Node 20, 22 and 24)
137
- and macOS (Node 24). Each job passed all 70 tests, 13 installed runtime checks
138
- and two typed consumers against the same archive. The Linux build verified
139
- 103 registry signatures and 38 attestations with zero known vulnerabilities.
140
- Counts apply to each platform's installed tree.
141
- - Workflow structure and documentation links passed local checks. Existing
142
- distributed archives retained their original SHA-256 hashes. The final
143
- documentation update records the tested commit; it does not change runtime code.
144
-
145
- Before public release, settle the license and whether to retain the older
146
- dotenvx decryption feature. The fresh native browser-login flow and live Linux
147
- keyring integration still need verification. Windows native login/storage is
148
- not implemented. Repository reporting/protection and npm publishing setup are
149
- also outstanding in [the release plan](releasing.md). The repository remains
150
- private and unpublished, with PR creation on hold.