@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
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ var promises = require('node:timers/promises');
4
+ var node_module = require('node:module');
5
+ var errors = require('../errors.cjs');
6
+ var values = require('../values.cjs');
7
+ var http = require('./http.cjs');
8
+ var system = require('./system.cjs');
9
+
10
+ const manifest = node_module.createRequire(require('node:url').pathToFileURL(__filename).href)('@ciqol/resenv/package.json');
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()) {
15
+ return Promise.resolve().then(() => {
16
+ // An explicitly selected identity is never replaced after an API rejection.
17
+ if (env.DOPPLER_TOKEN !== undefined)
18
+ return http.validateToken(env.DOPPLER_TOKEN);
19
+ let saved;
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();
30
+ if (token)
31
+ return token;
32
+ throw new errors.ResenvError('DOPPLER_AUTH_REQUIRED', 'Run resenv login doppler, or supply DOPPLER_TOKEN. Startup never opens a login prompt.');
33
+ });
34
+ }
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
+
124
+ exports.credential = credential;
125
+ exports.login = login;
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ var node_child_process = require('node:child_process');
4
+ var node_module = require('node:module');
5
+ var node_path = require('node:path');
6
+ var errors = require('../errors.cjs');
7
+ var assertSafeDiagnostics = require('../assert-safe-diagnostics.cjs');
8
+ var values = require('../values.cjs');
9
+ var references = require('./references.cjs');
10
+ var system = require('./system.cjs');
11
+
12
+ const require$1 = node_module.createRequire(require('node:url').pathToFileURL(__filename).href);
13
+ function resolveDopplerSync(env, run = node_child_process.spawnSync) {
14
+ const entries = references.references(env);
15
+ if (!entries.length)
16
+ return values.copyEnvironment({});
17
+ assertSafeDiagnostics.assertSafeDiagnostics();
18
+ const input = Object.fromEntries(entries.map(({ name }) => [name, env[name]]));
19
+ if (env.DOPPLER_TOKEN !== undefined)
20
+ input.DOPPLER_TOKEN = env.DOPPLER_TOKEN;
21
+ // Package self-resolution works both in the checkout and the packed artifact.
22
+ const manifestPath = require$1.resolve('@ciqol/resenv/package.json');
23
+ const manifest = require$1(manifestPath);
24
+ if (!values.isRecord(manifest) || typeof manifest.main !== 'string')
25
+ throw new errors.ResenvError('DOPPLER_RESOLUTION_FAILED', 'Invalid package entry point.');
26
+ const worker = node_path.join(node_path.dirname(manifestPath), node_path.dirname(manifest.main), 'doppler-worker.cjs');
27
+ const result = run(process.execPath, [worker], {
28
+ input: JSON.stringify(input), env: system.helperEnvironment(),
29
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], shell: false,
30
+ timeout: 60000, killSignal: 'SIGKILL', maxBuffer: 1048576,
31
+ detached: process.platform !== 'win32'
32
+ });
33
+ if (result.error || result.signal) {
34
+ if (result.pid && process.platform !== 'win32') {
35
+ try {
36
+ process.kill(-result.pid, 'SIGKILL');
37
+ }
38
+ catch (err) {
39
+ if (!values.hasErrorCode(err, 'ESRCH')) {
40
+ throw new errors.ResenvError('DOPPLER_RESOLUTION_FAILED', 'Could not stop resolver child processes after a resolution failure.');
41
+ }
42
+ }
43
+ }
44
+ throw new errors.ResenvError('DOPPLER_RESOLUTION_FAILED', 'Doppler resolution exceeded its startup time or output limit.');
45
+ }
46
+ let response;
47
+ try {
48
+ response = JSON.parse(result.stdout);
49
+ }
50
+ catch (err) {
51
+ if (!(err instanceof SyntaxError))
52
+ throw err;
53
+ }
54
+ const error = values.isRecord(response) ? response.error : undefined;
55
+ if (values.isRecord(error) && typeof error.code === 'string' && /^DOPPLER_[A-Z_]+$/.test(error.code) && typeof error.message === 'string') {
56
+ throw new errors.ResenvError(error.code, error.message);
57
+ }
58
+ const values$1 = values.isRecord(response) ? response.values : undefined;
59
+ if (result.status !== 0 || !values.isEnvironment(values$1) || Object.keys(values$1).length !== entries.length ||
60
+ entries.some(({ name }) => typeof values$1[name] !== 'string' || values$1[name]?.includes('\0'))) {
61
+ throw new errors.ResenvError('DOPPLER_RESOLUTION_FAILED', 'Doppler resolution returned an invalid result.');
62
+ }
63
+ return values$1;
64
+ }
65
+
66
+ exports.resolveDopplerSync = resolveDopplerSync;
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ var errors = require('../errors.cjs');
4
+ var values = require('../values.cjs');
5
+
6
+ const API_ORIGIN = 'https://api.doppler.com';
7
+ // No environment-controlled endpoint or redirects: credentials stay at Doppler.
8
+ function createClient(fetchImpl = globalThis.fetch) {
9
+ return async function request(path, { token, body, pending = false } = {}) {
10
+ let response;
11
+ try {
12
+ response = await fetchImpl(new URL(path, API_ORIGIN), {
13
+ method: body === undefined ? 'GET' : 'POST',
14
+ headers: {
15
+ 'content-type': 'application/json',
16
+ 'user-agent': 'ciqol-resenv',
17
+ ...(token ? { authorization: `Bearer ${token}` } : {})
18
+ },
19
+ body: body === undefined ? undefined : JSON.stringify(body),
20
+ redirect: 'error',
21
+ signal: AbortSignal.timeout(10000)
22
+ });
23
+ if (pending && response.status === 409) {
24
+ await response.body?.cancel();
25
+ return null;
26
+ }
27
+ if (!response.ok) {
28
+ await response.body?.cancel();
29
+ const code = response.status === 401 ? 'DOPPLER_AUTH_REQUIRED'
30
+ : response.status === 403 ? 'DOPPLER_ACCESS_DENIED'
31
+ : [404, 405, 501].includes(response.status) ? 'DOPPLER_ENDPOINT_UNAVAILABLE'
32
+ : 'DOPPLER_REQUEST_FAILED';
33
+ throw new errors.ResenvError(code, `Doppler request failed (HTTP ${response.status}). Check your login and access.`);
34
+ }
35
+ const chunks = [];
36
+ let size = 0;
37
+ if (!response.body)
38
+ throw new errors.ResenvError('DOPPLER_INVALID_RESPONSE', 'Doppler returned an empty response.');
39
+ for await (const raw of response.body) {
40
+ const chunk = raw;
41
+ if (!(chunk instanceof Uint8Array))
42
+ throw new errors.ResenvError('DOPPLER_INVALID_RESPONSE', 'Doppler returned invalid response bytes.');
43
+ size += chunk.length;
44
+ if (size > 1048576)
45
+ throw new errors.ResenvError('DOPPLER_INVALID_RESPONSE', 'Doppler response exceeded the size limit.');
46
+ chunks.push(chunk);
47
+ }
48
+ let result;
49
+ try {
50
+ result = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
51
+ }
52
+ catch (err) {
53
+ if (!(err instanceof TypeError || err instanceof SyntaxError))
54
+ throw err;
55
+ throw new errors.ResenvError('DOPPLER_INVALID_RESPONSE', 'Doppler returned invalid UTF-8 or JSON.');
56
+ }
57
+ if (!values.isRecord(result) || result.success === false || result.error) {
58
+ throw new errors.ResenvError('DOPPLER_INVALID_RESPONSE', 'Doppler returned an unsuccessful or malformed response.');
59
+ }
60
+ return result;
61
+ }
62
+ catch (err) {
63
+ if (err instanceof errors.ResenvError)
64
+ throw err;
65
+ throw new errors.ResenvError('DOPPLER_REQUEST_FAILED', 'Could not complete the HTTPS request to Doppler. Check your connection and retry.');
66
+ }
67
+ };
68
+ }
69
+ function validateToken(token) {
70
+ 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. Run resenv login doppler or supply a valid DOPPLER_TOKEN.');
72
+ }
73
+ return token;
74
+ }
75
+
76
+ exports.createClient = createClient;
77
+ exports.validateToken = validateToken;
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ var errors = require('../errors.cjs');
4
+
5
+ function references(env) {
6
+ const entries = Object.entries(env).filter((entry) => typeof entry[1] === 'string' && entry[1].startsWith('doppler://'));
7
+ if (entries.length > 128)
8
+ throw new errors.ResenvError('DOPPLER_TOO_MANY_REFERENCES', 'At most 128 Doppler references can be resolved per process.');
9
+ return entries.map(([name, value]) => {
10
+ // Parse the literal spelling: URL would lowercase hosts and normalize ../.
11
+ const match = /^doppler:\/\/([a-z0-9_-]+)\/([a-z0-9_-]+)\/([A-Za-z0-9_.:/%-]+)$/.exec(value);
12
+ let key;
13
+ try {
14
+ if (match?.[3])
15
+ key = decodeURIComponent(match[3]);
16
+ }
17
+ catch (err) {
18
+ if (!(err instanceof URIError))
19
+ throw err;
20
+ }
21
+ if (!match?.[1] || !match[2] || !key || key.length > 200 || !/^[A-Za-z0-9_.:/-]+$/.test(key) || key === '.' || key === '..') {
22
+ throw new errors.ResenvError('DOPPLER_INVALID_REFERENCE', `${JSON.stringify(name)} must use doppler://project/config/SECRET_NAME.`);
23
+ }
24
+ return { name, project: match[1], config: match[2], key };
25
+ });
26
+ }
27
+
28
+ exports.references = references;
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ var errors = require('../errors.cjs');
4
+ var values = require('../values.cjs');
5
+ var references = require('./references.cjs');
6
+ var auth = require('./auth.cjs');
7
+ var http = require('./http.cjs');
8
+
9
+ // 2026-10-06: cap startup fan-out at four to overlap latency without a burst of
10
+ // all 128 permitted references. The overall startup deadline still applies.
11
+ const CONCURRENT_REQUESTS = 4;
12
+ async function resolveReferences(env, { request = http.createClient(), getCredential = auth.credential } = {}) {
13
+ const entries = references.references(env);
14
+ const values$1 = values.copyEnvironment({});
15
+ if (!entries.length)
16
+ return values$1;
17
+ const groups = new Map();
18
+ for (const entry of entries) {
19
+ const query = new URLSearchParams({ project: entry.project, config: entry.config, name: entry.key });
20
+ const path = `/v3/configs/config/secret?${query.toString()}`;
21
+ const group = groups.get(path) || { path, names: [] };
22
+ group.names.push(entry.name);
23
+ groups.set(path, group);
24
+ }
25
+ const tasks = [...groups.values()];
26
+ const token = await getCredential(env);
27
+ let next = 0;
28
+ let failure;
29
+ const worker = async () => {
30
+ while (!failure && next < tasks.length) {
31
+ const task = tasks[next++];
32
+ if (!task)
33
+ return;
34
+ const { path, names } = task;
35
+ try {
36
+ const result = await request(path, { token });
37
+ const value = values.isRecord(result?.value) ? result.value.computed : undefined;
38
+ if (typeof value !== 'string' || !value.isWellFormed() || value.includes('\0') || Buffer.byteLength(value) > 65536) {
39
+ throw new errors.ResenvError('DOPPLER_INVALID_VALUE', `Doppler did not return a usable value for ${JSON.stringify(names[0])}.`);
40
+ }
41
+ for (const name of names)
42
+ values$1[name] = value;
43
+ }
44
+ catch (err) {
45
+ // Stop scheduling, but wait for already started requests to settle.
46
+ // No partial result escapes and no second credential is attempted.
47
+ failure ||= { err };
48
+ }
49
+ }
50
+ };
51
+ await Promise.all(Array.from({ length: Math.min(CONCURRENT_REQUESTS, tasks.length) }, worker));
52
+ if (failure) {
53
+ throw failure.err;
54
+ }
55
+ return values$1;
56
+ }
57
+
58
+ exports.resolveReferences = resolveReferences;
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ var node_child_process = require('node:child_process');
4
+ var node_fs = require('node:fs');
5
+ var node_os = require('node:os');
6
+ var errors = require('../errors.cjs');
7
+ var assertSafeDiagnostics = require('../assert-safe-diagnostics.cjs');
8
+ var http = require('./http.cjs');
9
+
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']) {
18
+ if (process.env[key])
19
+ env[key] = process.env[key];
20
+ }
21
+ return env;
22
+ }
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
+ }
71
+ function cliToken() {
72
+ if (!cli)
73
+ return undefined;
74
+ const result = invoke(cli, ['configure', 'get', 'token', '--plain']);
75
+ if (result.error || result.status !== 0 || !result.stdout.trim())
76
+ return undefined;
77
+ return http.validateToken(result.stdout.trim());
78
+ }
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 };
96
+ }
97
+
98
+ exports.createSystem = createSystem;
99
+ exports.helperEnvironment = helperEnvironment;
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ var node_fs = require('node:fs');
4
+ var errors = require('./errors.cjs');
5
+ var values = require('./values.cjs');
6
+ var resolve = require('./doppler/resolve.cjs');
7
+
8
+ // Private pipe protocol for the synchronous Node preload. Tokens and resolved
9
+ // values never appear in process arguments, inherited env, or worker diagnostics.
10
+ async function main() {
11
+ try {
12
+ const input = node_fs.readFileSync(0, 'utf8');
13
+ if (Buffer.byteLength(input) > 1048576)
14
+ throw new Error('Input exceeds limit');
15
+ const env = JSON.parse(input);
16
+ if (!values.isEnvironment(env))
17
+ throw new Error('Invalid environment input');
18
+ const values$1 = await resolve.resolveReferences(env);
19
+ process.stdout.write(JSON.stringify({ values: values$1 }));
20
+ }
21
+ catch (err) {
22
+ process.stdout.write(JSON.stringify({ error: err instanceof errors.ResenvError
23
+ ? { code: err.code, message: err.message }
24
+ : { code: 'DOPPLER_RESOLUTION_FAILED', message: 'Doppler resolution failed.' } }));
25
+ process.exitCode = 1;
26
+ }
27
+ }
28
+ void main();
@@ -0,0 +1 @@
1
+ import './doppler-worker.cjs';
package/errors.cjs ADDED
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ class ResenvError extends Error {
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.name = 'ResenvError';
8
+ this.code = code;
9
+ }
10
+ }
11
+
12
+ exports.ResenvError = ResenvError;
package/index.cjs ADDED
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ var node_util = require('node:util');
4
+ var commands = require('./commands.cjs');
5
+ var errors = require('./errors.cjs');
6
+ var references = require('./doppler/references.cjs');
7
+ var bridge = require('./doppler/bridge.cjs');
8
+ var values = require('./values.cjs');
9
+
10
+ const debug = node_util.debuglog('ciqol:resenv');
11
+ /** Always return a resolved copy; optionally commit replacements to the input. */
12
+ function resolve(env = process.env, options = {}) {
13
+ const replace = options.replace === true;
14
+ // Proxy traps can fail during a later write after earlier values were changed.
15
+ if (replace && node_util.types.isProxy(env)) {
16
+ throw new errors.ResenvError('INVALID_ENV_TARGET', 'In-place resolution requires process.env or a record with writable data properties, not a Proxy.');
17
+ }
18
+ const snapshot = values.copyEnvironment(env);
19
+ const commandReferences = options.commands === true
20
+ ? Object.values(snapshot).filter(value => typeof value === 'string' && value.includes('$(')).length
21
+ : 0;
22
+ const dopplerCount = Object.values(snapshot).filter(value => typeof value === 'string' && value.startsWith('doppler://')).length;
23
+ const total = commandReferences + dopplerCount;
24
+ if (total === 0) {
25
+ if (replace)
26
+ replaceEnvironment(env, snapshot);
27
+ debug('Nothing to handle.');
28
+ return snapshot;
29
+ }
30
+ try {
31
+ // Reject invalid references before any optional command can have effects.
32
+ const dopplerEntries = references.references(snapshot);
33
+ const resolved = options.commands === true
34
+ ? commands.resolveCommands(snapshot, options, debug)
35
+ : values.copyEnvironment(snapshot);
36
+ if (dopplerEntries.length) {
37
+ // Resolve only original references. Command and cloud output stays literal.
38
+ const cloudInput = Object.fromEntries(dopplerEntries.map(({ name }) => [name, snapshot[name]]));
39
+ if (resolved.DOPPLER_TOKEN !== undefined)
40
+ cloudInput.DOPPLER_TOKEN = resolved.DOPPLER_TOKEN;
41
+ let values;
42
+ try {
43
+ values = bridge.resolveDopplerSync(cloudInput);
44
+ }
45
+ catch (err) {
46
+ debug('Failed to resolve Doppler references.');
47
+ throw err;
48
+ }
49
+ for (const { name } of dopplerEntries) {
50
+ resolved[name] = values[name];
51
+ debug('Replaced secret value for %s.', JSON.stringify(name));
52
+ }
53
+ }
54
+ if (replace)
55
+ replaceEnvironment(env, resolved);
56
+ debug('Done: replaced %d secret value(s).', total);
57
+ return resolved;
58
+ }
59
+ catch (err) {
60
+ debug('Done: failed (%s); environment unchanged.', err instanceof errors.ResenvError ? err.code : 'STARTUP_FAILED');
61
+ throw err;
62
+ }
63
+ }
64
+ function replaceEnvironment(env, resolved) {
65
+ const updates = [];
66
+ for (const [name, value] of Object.entries(resolved)) {
67
+ const descriptor = Object.getOwnPropertyDescriptor(env, name);
68
+ if (descriptor && 'value' in descriptor && descriptor.value === value)
69
+ continue;
70
+ if (descriptor ? !descriptor.writable : !Object.isExtensible(env)) {
71
+ throw new errors.ResenvError('INVALID_ENV_TARGET', `Cannot replace ${JSON.stringify(name)} on a target without a writable data property.`);
72
+ }
73
+ updates.push([name, { ...(descriptor || { writable: true, enumerable: true, configurable: true }), value }]);
74
+ }
75
+ // Preflight every write. No setters or Proxy traps run during this commit.
76
+ for (const [name, descriptor] of updates) {
77
+ Object.defineProperty(env, name, descriptor);
78
+ }
79
+ }
80
+ function formatError(error) {
81
+ if (error instanceof errors.ResenvError)
82
+ return `[resenv] ${error.code}: ${error.message}`;
83
+ return '[resenv] STARTUP_FAILED: Environment resolution failed.';
84
+ }
85
+
86
+ exports.ResenvError = errors.ResenvError;
87
+ exports.formatError = formatError;
88
+ exports.resolve = resolve;
package/index.d.cts ADDED
@@ -0,0 +1 @@
1
+ export * from './types/src/index.js'
package/index.d.mts ADDED
@@ -0,0 +1 @@
1
+ export * from './types/src/index.js'
package/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { ResenvError, formatError, resolve } from './index.cjs';