@ciqol/resenv 0.1.0-beta.1 → 0.1.0-beta.2
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 +33 -13
- package/README.md +34 -20
- package/SECURITY.md +20 -20
- package/bin/resenv.cjs +21 -23
- package/docs/build.md +14 -15
- package/docs/dependencies.md +57 -501
- package/docs/doppler.md +69 -60
- package/docs/maintainer-handoff.md +29 -102
- package/docs/release-security.md +67 -131
- package/docs/releasing.md +32 -21
- package/doppler/auth.cjs +4 -110
- package/doppler/http.cjs +3 -8
- package/doppler/system.cjs +26 -75
- package/package.json +1 -1
- package/types/src/doppler/auth.d.ts +2 -17
- package/types/src/doppler/http.d.ts +1 -3
- package/types/src/doppler/system.d.ts +3 -8
- package/docs/maturity.md +0 -61
- package/docs/public-readiness.md +0 -138
- package/docs/security-performance-review.md +0 -150
- package/docs/validation.md +0 -700
package/docs/releasing.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Versions
|
|
4
4
|
|
|
5
|
-
`beta` is the sole
|
|
6
|
-
and use
|
|
5
|
+
`beta` is the sole release and default branch. Releases start at `0.1.0-beta.1`
|
|
6
|
+
and use npm tags `beta` and `latest`, both pointing to the same tested beta.
|
|
7
7
|
|
|
8
8
|
The planner uses semantic-release's official commit-analysis and release-notes
|
|
9
9
|
plugins. Conventional Commits decide whether changes need a release. Each
|
|
@@ -23,7 +23,7 @@ Never edit a generated manifest or overwrite a previously shared archive.
|
|
|
23
23
|
|
|
24
24
|
## CI pipeline
|
|
25
25
|
|
|
26
|
-
The [CI workflow](
|
|
26
|
+
The [CI workflow](https://github.com/ciqol/resenv/blob/beta/.github/workflows/ci.yml) builds one archive:
|
|
27
27
|
|
|
28
28
|
1. **Build:** normal npm installation, runtime/development audits, registry
|
|
29
29
|
signatures, lint, strict types and colocated Vitest specs. The read-only
|
|
@@ -40,9 +40,10 @@ The [CI workflow](../.github/workflows/ci.yml) builds one archive:
|
|
|
40
40
|
staging receipt; no Git tag or GitHub release is created yet.
|
|
41
41
|
4. **Approval and recording:** the maintainer reviews and approves the staged
|
|
42
42
|
package in npm using interactive 2FA. Wait for npm to make it available,
|
|
43
|
-
then rerun the original release job.
|
|
44
|
-
against the original tested archive
|
|
45
|
-
|
|
43
|
+
update `latest` to the approved version, then rerun the original release job.
|
|
44
|
+
It verifies the published integrity against the original tested archive and
|
|
45
|
+
requires npm tags `beta` and `latest` to point to that version before creating
|
|
46
|
+
its Git tag and GitHub prerelease. It does not stage or publish the package again.
|
|
46
47
|
|
|
47
48
|
Pushes, PRs and dispatches with `publish: false` cannot stage a package. Release
|
|
48
49
|
jobs are serialized and not cancelled. Artifacts last seven days. A fresh
|
|
@@ -74,7 +75,7 @@ setup, set Actions variable `RELEASE_ENABLED=true`. Leave it unset until then
|
|
|
74
75
|
to prevent a dispatch from creating an unrestricted environment automatically.
|
|
75
76
|
Environment creation or protection changes require explicit authorization.
|
|
76
77
|
|
|
77
|
-
## Run
|
|
78
|
+
## Run a beta release
|
|
78
79
|
|
|
79
80
|
Open [CI](https://github.com/ciqol/resenv/actions/workflows/ci.yml), choose
|
|
80
81
|
**Run workflow**, select `beta`, and enable the staging input. When build and
|
|
@@ -91,8 +92,15 @@ npm stage approve <stage-id>
|
|
|
91
92
|
|
|
92
93
|
These are maintainer review/approval commands; the archive is built and staged
|
|
93
94
|
by GitHub Actions. OIDC cannot perform the approval step. Wait for npm scanning
|
|
94
|
-
and public availability, then
|
|
95
|
-
|
|
95
|
+
and public availability, then update the default install tag in the maintainer
|
|
96
|
+
session:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
npm dist-tag add @ciqol/resenv@<approved-version> latest
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Rerun the original release job to record the release. Verify the npm version,
|
|
103
|
+
`beta` and `latest` tags, archive integrity, provenance and GitHub
|
|
96
104
|
prerelease. Never infer success from a staging receipt alone.
|
|
97
105
|
|
|
98
106
|
For a hosted rehearsal, leave the staging input off. Local verification is:
|
|
@@ -118,8 +126,8 @@ matches its candidate. A conflicting archive, changed beta head or conflicting
|
|
|
118
126
|
Git tag stops recovery. Keep the beta branch at the reviewed commit until the
|
|
119
127
|
release is recorded. If its artifact expires, review recovery separately.
|
|
120
128
|
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
Retain the `0.0.0-stage` placeholder until a functional beta is publicly
|
|
130
|
+
available and verified.
|
|
123
131
|
With explicit authorization, remove that exact placeholder version and check
|
|
124
132
|
remaining versions and tags. Never unpublish the whole package. npm's unpublish
|
|
125
133
|
policy still applies; deleted versions cannot be reused.
|
|
@@ -127,16 +135,19 @@ See [npm's policy](https://docs.npmjs.com/policies/unpublish/).
|
|
|
127
135
|
|
|
128
136
|
## Security and platform limits
|
|
129
137
|
|
|
130
|
-
The runtime package has no npm dependencies or advisory exceptions.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
CLI
|
|
139
|
-
|
|
138
|
+
The runtime package has no npm dependencies or advisory exceptions. Installed
|
|
139
|
+
runtime and development audits must pass with the unused engine/publisher peer graph
|
|
140
|
+
omitted. Audit and planning enforce that absence; the lockfile still records
|
|
141
|
+
the uninstalled peers. The existing November 9, 2026 review deadline remains
|
|
142
|
+
for the policy and separately reviewed platform npm. The privileged job does
|
|
143
|
+
not install release-tool modules. See [release security](release-security.md).
|
|
144
|
+
|
|
145
|
+
Doppler is the only built-in cloud provider. Authentication is owned by the
|
|
146
|
+
Doppler CLI; resenv reads its existing directory-scoped token or accepts
|
|
147
|
+
`DOPPLER_TOKEN` for unattended use. It does not manage native credential storage
|
|
148
|
+
or browser login. Standard Windows CLI discovery has injected coverage but no
|
|
149
|
+
native Windows end-to-end verification. Commands require explicit opt-in.
|
|
150
|
+
Dotenvx ciphertext is left unchanged. See [Security](../SECURITY.md).
|
|
140
151
|
|
|
141
152
|
Sources: [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/),
|
|
142
153
|
[staged publishing](https://docs.npmjs.com/cli/v11/commands/npm-stage/),
|
package/doppler/auth.cjs
CHANGED
|
@@ -1,125 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var promises = require('node:timers/promises');
|
|
4
|
-
var node_module = require('node:module');
|
|
5
3
|
var errors = require('../errors.cjs');
|
|
6
|
-
var values = require('../values.cjs');
|
|
7
4
|
var http = require('./http.cjs');
|
|
8
5
|
var system = require('./system.cjs');
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
if (!values.isRecord(manifest) || typeof manifest.version !== 'string')
|
|
12
|
-
throw new Error('Invalid package version');
|
|
13
|
-
const { version } = manifest;
|
|
14
|
-
function credential(env, system$1 = system.createSystem()) {
|
|
7
|
+
function credential(env, system$1) {
|
|
15
8
|
return Promise.resolve().then(() => {
|
|
16
|
-
// An
|
|
9
|
+
// An explicit identity is never replaced after validation or API rejection.
|
|
17
10
|
if (env.DOPPLER_TOKEN !== undefined)
|
|
18
11
|
return http.validateToken(env.DOPPLER_TOKEN);
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
saved = system$1.read();
|
|
22
|
-
}
|
|
23
|
-
catch (err) {
|
|
24
|
-
if (!values.hasErrorCode(err, 'DOPPLER_KEYRING_UNAVAILABLE'))
|
|
25
|
-
throw err;
|
|
26
|
-
}
|
|
27
|
-
if (saved)
|
|
28
|
-
return saved;
|
|
29
|
-
const token = system$1.cliToken();
|
|
12
|
+
const token = (system$1 || system.createSystem()).cliToken();
|
|
30
13
|
if (token)
|
|
31
14
|
return token;
|
|
32
|
-
throw new errors.ResenvError('DOPPLER_AUTH_REQUIRED', 'Run
|
|
15
|
+
throw new errors.ResenvError('DOPPLER_AUTH_REQUIRED', 'Run doppler login from your application directory, or supply DOPPLER_TOKEN. Startup never opens a login prompt.');
|
|
33
16
|
});
|
|
34
17
|
}
|
|
35
|
-
async function login({ cli = false, noBrowser = false } = {}, { request = http.createClient(), system: system$1 = system.createSystem(), output = text => process.stderr.write(`${text}\n`), interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY), env = process.env, sleep = promises.setTimeout, now = Date.now } = {}) {
|
|
36
|
-
if (!interactive || env.CI)
|
|
37
|
-
throw new errors.ResenvError('DOPPLER_INTERACTIVE_REQUIRED', 'Login requires an interactive local terminal outside CI. Supply DOPPLER_TOKEN for unattended runs.');
|
|
38
|
-
if (env.DOPPLER_TOKEN !== undefined)
|
|
39
|
-
throw new errors.ResenvError('DOPPLER_TOKEN_OVERRIDE', 'Unset DOPPLER_TOKEN before logging in; it overrides saved credentials.');
|
|
40
|
-
const fallback = ({ storeUnavailable = false } = {}) => {
|
|
41
|
-
output('Using Doppler CLI login.');
|
|
42
|
-
system$1.cliLogin();
|
|
43
|
-
if (system$1.canStore && !storeUnavailable) {
|
|
44
|
-
// Keep the identity chosen by this login ahead of an older resenv token.
|
|
45
|
-
try {
|
|
46
|
-
system$1.write(http.validateToken(system$1.cliToken()));
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
throw new errors.ResenvError('DOPPLER_KEYRING_UNAVAILABLE', 'CLI login completed, but resenv could not update its saved credential. Unlock the credential store and retry.');
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
output('Done: Doppler CLI login completed.');
|
|
53
|
-
return 'cli';
|
|
54
|
-
};
|
|
55
|
-
if (cli || !system$1.canStore)
|
|
56
|
-
return fallback();
|
|
57
|
-
// Check keyring access before creating an authorization session.
|
|
58
|
-
try {
|
|
59
|
-
system$1.read();
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
if (!values.hasErrorCode(err, 'DOPPLER_KEYRING_UNAVAILABLE'))
|
|
63
|
-
throw err;
|
|
64
|
-
return fallback({ storeUnavailable: true });
|
|
65
|
-
}
|
|
66
|
-
let challenge;
|
|
67
|
-
try {
|
|
68
|
-
// Observed in Doppler's official MCP client, not a public OAuth contract.
|
|
69
|
-
// Identify resenv honestly rather than impersonating Doppler's MCP client.
|
|
70
|
-
const query = new URLSearchParams({ hostname: 'resenv', version: `v${version}`, os: process.platform, arch: process.arch });
|
|
71
|
-
challenge = await request(`/v3/auth/cli/generate/2?${query.toString()}`);
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
if (values.hasErrorCode(err, 'DOPPLER_ENDPOINT_UNAVAILABLE'))
|
|
75
|
-
return fallback();
|
|
76
|
-
throw err;
|
|
77
|
-
}
|
|
78
|
-
let url;
|
|
79
|
-
try {
|
|
80
|
-
url = new URL(typeof challenge?.auth_url === 'string' ? challenge.auth_url : '');
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
if (!(err instanceof TypeError))
|
|
84
|
-
throw err;
|
|
85
|
-
}
|
|
86
|
-
if (!challenge || !url || url.origin !== 'https://dashboard.doppler.com' || url.username || url.password || url.pathname !== '/workplace/auth/cli' ||
|
|
87
|
-
typeof challenge.code !== 'string' || !/^[A-Za-z0-9_-]{8,256}$/.test(challenge.code) ||
|
|
88
|
-
typeof challenge.polling_code !== 'string' || !/^[A-Za-z0-9_.-]{8,512}$/.test(challenge.polling_code)) {
|
|
89
|
-
throw new errors.ResenvError('DOPPLER_INVALID_RESPONSE', 'Doppler returned an invalid browser authorization challenge.');
|
|
90
|
-
}
|
|
91
|
-
// Display the human authorization code only in the explicit login command.
|
|
92
|
-
output(`Authorize resenv at ${url.href}`);
|
|
93
|
-
output(`Authorization code: ${challenge.code}`);
|
|
94
|
-
if (!noBrowser)
|
|
95
|
-
system$1.openBrowser(url.href);
|
|
96
|
-
const deadline = now() + 300000;
|
|
97
|
-
while (now() < deadline) {
|
|
98
|
-
const result = await request('/v3/auth/cli/authorize', { body: { code: challenge.polling_code }, pending: true });
|
|
99
|
-
if (result) {
|
|
100
|
-
const token = http.validateToken(result.token);
|
|
101
|
-
try {
|
|
102
|
-
system$1.write(token);
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// Do not leave a newly minted, unpersisted credential active if storage fails.
|
|
106
|
-
try {
|
|
107
|
-
await request('/v3/auth/revoke', { token, body: { token } });
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
if (!(err instanceof errors.ResenvError)) {
|
|
111
|
-
throw new errors.ResenvError('DOPPLER_KEYRING_UNAVAILABLE', 'Could not save or revoke the login credential. Check Doppler CLI Tokens before retrying.');
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
throw new errors.ResenvError('DOPPLER_KEYRING_UNAVAILABLE', 'Could not save the login credential. Revocation was attempted; check Doppler CLI Tokens, then try resenv login doppler --cli.');
|
|
115
|
-
}
|
|
116
|
-
output('Done: Doppler login saved in the OS credential store.');
|
|
117
|
-
return 'native';
|
|
118
|
-
}
|
|
119
|
-
await sleep(2000);
|
|
120
|
-
}
|
|
121
|
-
throw new errors.ResenvError('DOPPLER_LOGIN_TIMEOUT', 'Doppler login timed out. Run resenv login doppler to try again.');
|
|
122
|
-
}
|
|
123
18
|
|
|
124
19
|
exports.credential = credential;
|
|
125
|
-
exports.login = login;
|
package/doppler/http.cjs
CHANGED
|
@@ -6,24 +6,19 @@ var values = require('../values.cjs');
|
|
|
6
6
|
const API_ORIGIN = 'https://api.doppler.com';
|
|
7
7
|
// No environment-controlled endpoint or redirects: credentials stay at Doppler.
|
|
8
8
|
function createClient(fetchImpl = globalThis.fetch) {
|
|
9
|
-
return async function request(path, { token
|
|
9
|
+
return async function request(path, { token } = {}) {
|
|
10
10
|
let response;
|
|
11
11
|
try {
|
|
12
12
|
response = await fetchImpl(new URL(path, API_ORIGIN), {
|
|
13
|
-
method:
|
|
13
|
+
method: 'GET',
|
|
14
14
|
headers: {
|
|
15
15
|
'content-type': 'application/json',
|
|
16
16
|
'user-agent': 'ciqol-resenv',
|
|
17
17
|
...(token ? { authorization: `Bearer ${token}` } : {})
|
|
18
18
|
},
|
|
19
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
20
19
|
redirect: 'error',
|
|
21
20
|
signal: AbortSignal.timeout(10000)
|
|
22
21
|
});
|
|
23
|
-
if (pending && response.status === 409) {
|
|
24
|
-
await response.body?.cancel();
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
22
|
if (!response.ok) {
|
|
28
23
|
await response.body?.cancel();
|
|
29
24
|
const code = response.status === 401 ? 'DOPPLER_AUTH_REQUIRED'
|
|
@@ -68,7 +63,7 @@ function createClient(fetchImpl = globalThis.fetch) {
|
|
|
68
63
|
}
|
|
69
64
|
function validateToken(token) {
|
|
70
65
|
if (typeof token !== 'string' || !/^dp\.[a-z]+\.[A-Za-z0-9_.-]{10,512}$/.test(token)) {
|
|
71
|
-
throw new errors.ResenvError('DOPPLER_INVALID_TOKEN', 'The Doppler credential is malformed.
|
|
66
|
+
throw new errors.ResenvError('DOPPLER_INVALID_TOKEN', 'The Doppler credential is malformed. Check your Doppler CLI login or supply a valid DOPPLER_TOKEN.');
|
|
72
67
|
}
|
|
73
68
|
return token;
|
|
74
69
|
}
|
package/doppler/system.cjs
CHANGED
|
@@ -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
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
74
|
-
const result =
|
|
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
|
-
|
|
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,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
|
-
|
|
5
|
-
|
|
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
|
|
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.
|