FIXED proxy not refreshing from terminal

RESOLVED pr conversations

ADD test cases and os specific test casess

REVERTE comment removed for windows refetch

refactor: remove redundant afterEach cleanup in shell-env-proxy-refresh tests

RENAME the  test file and add more test for wider coverage among the test cases

test: add integration tests for refreshShellEnvProxyVars

delete: remove outdated system proxy integration test file

fix: enhance proxy variable restoration on shell-env subprocess failure

fix: implement timeout for shellEnv to prevent hanging on misconfigured shells

feat: add refreshShellEnvProxyVars function to shell-env-state.ts and update shell environment initialization logic

refactor: simplify shell environment refresh and update system proxy integration

refactor: implement withTimeout utility for shell environment operations to prevent indefinite hangs
This commit is contained in:
abhishekp-bruno
2026-06-24 16:53:08 +05:30
committed by Sid
parent 7a1ea5e381
commit 76f11e0385
5 changed files with 579 additions and 6 deletions

View File

@@ -1,10 +1,13 @@
const { getSystemProxy } = require('@usebruno/requests');
const { getSystemProxy, refreshShellEnvProxyVars } = require('@usebruno/requests');
let cachedSystemProxy;
let systemProxyPromise;
const loadSystemProxy = async () => {
const loadSystemProxy = async ({ refreshShellEnv = false } = {}) => {
try {
if (refreshShellEnv) {
await refreshShellEnvProxyVars();
}
cachedSystemProxy = await getSystemProxy();
} catch (error) {
console.error('Failed to initialize system proxy:', error);
@@ -21,7 +24,7 @@ const loadSystemProxy = async () => {
const fetchSystemProxy = ({ refresh = false } = {}) => {
if (refresh || !systemProxyPromise) {
systemProxyPromise = loadSystemProxy();
systemProxyPromise = loadSystemProxy({ refreshShellEnv: refresh });
}
return systemProxyPromise;
};

View File

@@ -8,7 +8,7 @@ export { transformProxyConfig } from './utils/proxy-util';
export { default as createVaultClient, VaultError } from './utils/node-vault';
export type { VaultClient, VaultConfig, VaultRequestOptions } from './utils/node-vault';
export { getHttpHttpsAgents, resolveAgentsFromPac, PatchedHttpsProxyAgent } from './utils/http-https-agents';
export { initializeShellEnv } from './utils/shell-env';
export { initializeShellEnv, refreshShellEnvProxyVars } from './utils/shell-env';
export { getOrCreateHttpsAgent, getOrCreateHttpAgent, clearAgentCache, getAgentCacheSize } from './utils/agent-cache';
export { getPacResolver, clearPacCache } from './utils/pac-resolver';
export type { PacWrapper, GetPacResolverParams } from './utils/pac-resolver';

View File

@@ -0,0 +1,282 @@
/**
* Composition tests: refreshShellEnvProxyVars + getSystemProxy wired end-to-end.
*
* The individual contracts are covered by:
* - ./get-system-proxy.spec.ts (cross-platform detection + merge)
* - ../../utils/shell-env-proxy-refresh.spec.ts (refresh delete/re-populate + Windows early-return)
*
* This file exercises only the composition: after a refresh, does the resulting proxy config
* from getSystemProxy reflect the intended end-to-end behavior?
*/
export { }; // Mark this file as a module so top-level declarations don't collide with sibling specs.
jest.mock('node:child_process', () => ({ execFile: jest.fn() }));
jest.mock('node:util', () => ({ promisify: jest.fn((fn) => fn) }));
jest.mock('node:fs', () => ({ existsSync: jest.fn() }));
jest.mock('node:fs/promises', () => ({ readFile: jest.fn(), readdir: jest.fn() }));
// Config the mocked login shell would export. Reassigned per test.
// Prefix required by jest.mock's out-of-scope-variable guard (mock*/MOCK* allowed).
let mockShellEnvResult: Record<string, string> = {};
jest.mock('shell-env', () => ({
shellEnv: () => Promise.resolve(mockShellEnvResult)
}));
const PROXY_ENV_KEYS = [
'http_proxy',
'HTTP_PROXY',
'https_proxy',
'HTTPS_PROXY',
'no_proxy',
'NO_PROXY',
'all_proxy',
'ALL_PROXY'
];
interface FreshModules {
execFile: jest.MockedFunction<any>;
existsSync: jest.MockedFunction<any>;
refresh: () => Promise<Record<string, string>>;
getSystemProxy: () => Promise<any>;
}
/**
* Re-require the proxy modules with a mocked node:os platform. SystemProxyResolver reads
* os.platform() at construction, so this has to happen before requiring ./index.
* refreshShellEnvProxyVars branches on process.platform (not os.platform), so Windows tests
* must additionally override process.platform via Object.defineProperty.
*/
const loadForPlatform = (platform: string): FreshModules => {
jest.resetModules();
jest.doMock('node:os', () => ({ platform: () => platform }));
const { execFile } = require('node:child_process');
const { existsSync } = require('node:fs');
const { refreshShellEnvProxyVars } = require('../../utils/shell-env');
const { getSystemProxy } = require('./index');
return { execFile, existsSync, refresh: refreshShellEnvProxyVars, getSystemProxy };
};
/**
* Argument-aware execFile router. Matches on a substring of the joined command; first hit wins.
* Keeps tests decoupled from the resolver's internal call sequencing.
*/
const routeExecFile = (
execFile: jest.MockedFunction<any>,
routes: Array<[string, { stdout: string; stderr?: string } | Error]>
) => {
execFile.mockImplementation((file: string, args: string[]) => {
const command = [file, ...args].join(' ');
for (const [needle, response] of routes) {
if (command.includes(needle)) {
return response instanceof Error
? Promise.reject(response)
: Promise.resolve({ stderr: '', ...response });
}
}
return Promise.resolve({ stdout: '', stderr: '' });
});
};
let originalEnv: typeof process.env;
beforeEach(() => {
originalEnv = { ...process.env };
mockShellEnvResult = {};
for (const key of PROXY_ENV_KEYS) {
delete process.env[key];
}
});
afterEach(() => {
process.env = originalEnv;
});
describe('refresh + getSystemProxy composition', () => {
it('macOS: stale env purged on refresh, system layer wins alone', async () => {
// A proxy var lingering on process.env from a previous session — the user has since
// removed the export from .zshrc, so the login shell no longer emits it.
process.env.http_proxy = 'http://stale.usebruno.com:8080';
mockShellEnvResult = {};
const { execFile, refresh, getSystemProxy } = loadForPlatform('darwin');
execFile.mockResolvedValueOnce({
stdout: `<dictionary> {
HTTPEnable : 1
HTTPPort : 8080
HTTPProxy : sys-proxy.usebruno.com
}`,
stderr: ''
});
await refresh();
const result = await getSystemProxy();
// Stale env value is gone, so system detection wins with no "+ environment" suffix.
expect(result.http_proxy).toBe('http://sys-proxy.usebruno.com:8080');
expect(result.source).toBe('macos-system');
});
it('macOS: profile export beats system detection after refresh', async () => {
// .zshrc now exports HTTP_PROXY (either newly added or edited).
mockShellEnvResult = { http_proxy: 'http://profile.usebruno.com:9090' };
const { execFile, refresh, getSystemProxy } = loadForPlatform('darwin');
execFile.mockResolvedValueOnce({
stdout: `<dictionary> {
HTTPEnable : 1
HTTPPort : 8080
HTTPProxy : sys-proxy.usebruno.com
}`,
stderr: ''
});
await refresh();
const result = await getSystemProxy();
expect(result.http_proxy).toBe('http://profile.usebruno.com:9090');
expect(result.source).toBe('macos-system + environment');
});
it('Linux: profile export merges with gsettings detection — env wins http, system supplies https + bypass', async () => {
// .zshrc exports an http proxy; gsettings reports a DIFFERENT http proxy plus https + bypass
// the profile does not set. After refresh, the env layer should win for http and combine
// with system detection for the rest.
mockShellEnvResult = { http_proxy: 'http://profile.usebruno.com:9090' };
const { execFile, existsSync, refresh, getSystemProxy } = loadForPlatform('linux');
existsSync.mockReturnValue(false); // isolate the command-based (gsettings) path
routeExecFile(execFile, [
['org.gnome.system.proxy mode', { stdout: '\'manual\'' }],
['org.gnome.system.proxy.http host', { stdout: '\'sys-proxy.usebruno.com\'' }],
['org.gnome.system.proxy.http port', { stdout: '8080' }],
['org.gnome.system.proxy.https host', { stdout: '\'secure-proxy.usebruno.com\'' }],
['org.gnome.system.proxy.https port', { stdout: '8443' }],
['org.gnome.system.proxy ignore-hosts', { stdout: '[\'localhost\', \'127.0.0.1\']' }]
]);
await refresh();
const result = await getSystemProxy();
expect(result.http_proxy).toBe('http://profile.usebruno.com:9090'); // env wins
expect(result.https_proxy).toBe('http://secure-proxy.usebruno.com:8443'); // from system
expect(result.no_proxy).toBe('localhost,127.0.0.1'); // from system
expect(result.source).toBe('linux-system + environment');
});
it('Linux: stale env purged on refresh, gsettings detection wins alone', async () => {
// A proxy var lingering on process.env from a previous session — the user has since
// removed the export from their profile, so the login shell no longer emits it.
process.env.http_proxy = 'http://stale.usebruno.com:1234';
mockShellEnvResult = {};
const { execFile, existsSync, refresh, getSystemProxy } = loadForPlatform('linux');
existsSync.mockReturnValue(false);
routeExecFile(execFile, [
['org.gnome.system.proxy mode', { stdout: '\'manual\'' }],
['org.gnome.system.proxy.http host', { stdout: '\'sys-proxy.usebruno.com\'' }],
['org.gnome.system.proxy.http port', { stdout: '8080' }]
]);
await refresh();
const result = await getSystemProxy();
// Stale env value is gone, so system detection wins with no "+ environment" suffix.
expect(result.http_proxy).toBe('http://sys-proxy.usebruno.com:8080');
expect(result.source).toBe('linux-system');
});
it('Linux (KDE): profile export merges with kreadconfig5 detection — env wins http, system supplies https + bypass', async () => {
// gsettings is unavailable, so detection falls through to KDE (kreadconfig5). The profile
// exports an http proxy; KDE reports a DIFFERENT http proxy plus https + bypass.
mockShellEnvResult = { http_proxy: 'http://profile.usebruno.com:9090' };
const { execFile, existsSync, refresh, getSystemProxy } = loadForPlatform('linux');
existsSync.mockReturnValue(false); // isolate the command-based path
execFile.mockImplementation((file: string, args: string[]) => {
const fullCommand = [file, ...args].join(' ');
if (fullCommand.includes('gsettings')) {
return Promise.reject(new Error('gsettings not available'));
}
if (fullCommand.includes('kreadconfig5')) {
// Lowercase args before matching so we are agnostic to the key casing the resolver uses.
const lowerArgs = args.join(' ').toLowerCase();
if (lowerArgs.includes('proxytype')) return Promise.resolve({ stdout: '1', stderr: '' });
if (lowerArgs.includes('httpproxy')) return Promise.resolve({ stdout: 'http://sys-proxy.usebruno.com:8080', stderr: '' });
if (lowerArgs.includes('httpsproxy')) return Promise.resolve({ stdout: 'http://secure-proxy.usebruno.com:8443', stderr: '' });
if (lowerArgs.includes('noproxyfor')) return Promise.resolve({ stdout: 'localhost,127.0.0.1', stderr: '' });
}
return Promise.resolve({ stdout: '', stderr: '' });
});
await refresh();
const result = await getSystemProxy();
expect(result.http_proxy).toBe('http://profile.usebruno.com:9090'); // env wins
expect(result.https_proxy).toBe('http://secure-proxy.usebruno.com:8443'); // from system (KDE)
expect(result.no_proxy).toBe('localhost,127.0.0.1'); // from system (KDE)
expect(result.source).toBe('linux-system + environment');
});
it('Windows: shell profile export is ignored, registry detection wins', async () => {
// fetchShellEnv branches on process.platform (not the mocked node:os), so we force it here.
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
try {
// A shell profile that WOULD export a proxy — on Windows the refresh is a no-op, so it
// must not surface. Registry detection alone should decide the result.
mockShellEnvResult = { http_proxy: 'http://should-be-ignored.usebruno.com:9090' };
const { execFile, refresh, getSystemProxy } = loadForPlatform('win32');
routeExecFile(execFile, [
['Internet Settings', {
stdout: `
HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
ProxyEnable REG_DWORD 0x1
ProxyServer REG_SZ sys-proxy.usebruno.com:8080
`
}]
]);
await refresh();
const result = await getSystemProxy();
// Registry value surfaces; the ignored shell value contributes nothing, so no "+ environment".
expect(result.http_proxy).toBe('http://sys-proxy.usebruno.com:8080');
expect(result.source).toBe('windows-system');
} finally {
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
}
});
it('Windows: launcher-set env survives refresh and surfaces via getSystemProxy', async () => {
// fetchShellEnv branches on process.platform (not the mocked node:os), so we force it here.
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
try {
// Set by a launcher script / parent shell / registry propagation — anything that reaches
// process.env before Bruno starts. Without the Windows early-return in
// refreshShellEnvProxyVars, this value would be stripped and never restored.
process.env.HTTP_PROXY = 'http://launcher.usebruno.com:8080';
// A shell profile that WOULD export a different proxy — must not surface on Windows.
mockShellEnvResult = { http_proxy: 'http://should-be-ignored.usebruno.com:9090' };
const { execFile, refresh, getSystemProxy } = loadForPlatform('win32');
// System registry has no proxy, so any surviving proxy in the final result must come
// from the env layer we preserved.
execFile.mockImplementation(() => Promise.resolve({ stdout: '', stderr: '' }));
await refresh();
const result = await getSystemProxy();
expect(result.http_proxy).toBe('http://launcher.usebruno.com:8080');
expect(result.source).toContain('environment');
} finally {
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
}
});
});

View File

@@ -0,0 +1,201 @@
/**
* Regression tests for refreshShellEnvProxyVars.
*
* shell-env spawns a login shell (`$SHELL -i -c 'export -p'`) that INHERITS the parent
* process.env. So any proxy var still set on process.env when the shell is spawned would be
* inherited and re-exported — meaning a proxy the user removed from their profile would never
* go away. refreshShellEnvProxyVars guards against this by deleting all proxy vars BEFORE
* invoking the shell on POSIX platforms, and by early-returning on Windows (where fetchShellEnv
* is a no-op and the delete would strip values with nothing to restore them).
*
* The mock for shell-env models login-shell parent-env inheritance and captures a snapshot of
* process.env at invocation time, so tests can assert delete-before-fetch ordering directly.
*/
import { refreshShellEnvProxyVars, PROXY_ENV_KEYS } from './shell-env';
let mockShellEnvResult: Record<string, string> = {};
// When set, the mocked shellEnv() rejects with this error. Lets us verify graceful degradation.
let mockShellEnvThrows: Error | null = null;
// Snapshot of the proxy vars present in process.env when shellEnv() was invoked. Lets tests
// assert that refreshShellEnvProxyVars deleted stale vars BEFORE spawning the shell.
// Prefix required by jest.mock's out-of-scope-variable guard (mock*/MOCK* allowed).
let mockProxyVarsSeenByShell: Record<string, string> = {};
jest.mock('shell-env', () => ({
shellEnv: () => {
const PROXY_KEYS = [
'http_proxy',
'HTTP_PROXY',
'https_proxy',
'HTTPS_PROXY',
'no_proxy',
'NO_PROXY',
'all_proxy',
'ALL_PROXY'
];
// Capture what the subprocess would inherit from Bruno's process.env at spawn time.
mockProxyVarsSeenByShell = {};
for (const key of PROXY_KEYS) {
if (process.env[key] !== undefined) {
mockProxyVarsSeenByShell[key] = process.env[key] as string;
}
}
if (mockShellEnvThrows) {
return Promise.reject(mockShellEnvThrows);
}
// Model a login shell: inherits from parent, then applies whatever the profile exports.
return Promise.resolve({ ...mockProxyVarsSeenByShell, ...mockShellEnvResult });
}
}));
describe('refreshShellEnvProxyVars — shell inheritance (POSIX)', () => {
beforeEach(() => {
mockProxyVarsSeenByShell = {};
mockShellEnvResult = {};
mockShellEnvThrows = null;
for (const key of PROXY_ENV_KEYS) {
delete process.env[key];
}
});
test('updates proxy env vars from shell config', async () => {
process.env.http_proxy = 'http://old-proxy:8080';
mockShellEnvResult = {
http_proxy: 'http://new-proxy:8080',
https_proxy: 'http://new-proxy:8443'
};
await refreshShellEnvProxyVars();
expect(process.env.http_proxy).toBe('http://new-proxy:8080');
expect(process.env.https_proxy).toBe('http://new-proxy:8443');
});
test('removes proxy env vars missing from shell config', async () => {
// Stale vars lingering from a previous session.
process.env.http_proxy = 'http://old-proxy:8080';
process.env.no_proxy = 'localhost';
// User removed the exports from their profile -> shell config no longer has them.
mockShellEnvResult = {};
await refreshShellEnvProxyVars();
expect(process.env.http_proxy).toBeUndefined();
expect(process.env.no_proxy).toBeUndefined();
});
test('treats an empty-string proxy value as removal', async () => {
process.env.http_proxy = 'http://old-proxy:8080';
mockShellEnvResult = { http_proxy: '' };
await refreshShellEnvProxyVars();
expect(process.env.http_proxy).toBeUndefined();
});
test('leaves non-proxy env vars untouched', async () => {
process.env.NON_PROXY_VAR = 'keep-me';
mockShellEnvResult = { http_proxy: 'http://new-proxy:8080' };
await refreshShellEnvProxyVars();
expect(process.env.http_proxy).toBe('http://new-proxy:8080');
expect(process.env.NON_PROXY_VAR).toBe('keep-me');
delete process.env.NON_PROXY_VAR;
});
test('handles uppercase proxy variants (HTTP_PROXY, HTTPS_PROXY, NO_PROXY, ALL_PROXY)', async () => {
process.env.HTTP_PROXY = 'http://old-upper:8080';
process.env.ALL_PROXY = 'http://old-all:1080';
mockShellEnvResult = {
HTTP_PROXY: 'http://new-upper:8080',
HTTPS_PROXY: 'http://new-upper:8443',
NO_PROXY: 'localhost,127.0.0.1',
ALL_PROXY: 'http://new-all:1080'
};
await refreshShellEnvProxyVars();
expect(process.env.HTTP_PROXY).toBe('http://new-upper:8080');
expect(process.env.HTTPS_PROXY).toBe('http://new-upper:8443');
expect(process.env.NO_PROXY).toBe('localhost,127.0.0.1');
expect(process.env.ALL_PROXY).toBe('http://new-all:1080');
});
test('restores prior proxy vars when the shell-env subprocess fails', async () => {
process.env.http_proxy = 'http://existing:8080';
mockShellEnvThrows = new Error('shell subprocess failed');
// On subprocess failure we return {} but restore the snapshot taken before the
// delete, so the user is not left silently unproxied.
await expect(refreshShellEnvProxyVars()).resolves.toEqual({});
expect(process.env.http_proxy).toBe('http://existing:8080');
});
test('returns the raw shell env dict, including non-proxy keys', async () => {
mockShellEnvResult = {
http_proxy: 'http://p:8080',
PATH: '/foo:/bar',
EDITOR: 'vim'
};
const result = await refreshShellEnvProxyVars();
expect(result).toEqual({
http_proxy: 'http://p:8080',
PATH: '/foo:/bar',
EDITOR: 'vim'
});
});
});
describe('refreshShellEnvProxyVars — Windows', () => {
const originalPlatform = process.platform;
beforeAll(() => {
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
});
afterAll(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
});
beforeEach(() => {
mockProxyVarsSeenByShell = {};
mockShellEnvResult = {};
mockShellEnvThrows = null;
for (const key of PROXY_ENV_KEYS) {
delete process.env[key];
}
});
test('leaves process.env proxy vars untouched (registry / launcher / parent-shell values survive)', async () => {
// A proxy in process.env from any Windows source: registry propagation, launcher script,
// PowerShell $env, Git Bash export, etc. All look the same to us.
process.env.HTTP_PROXY = 'http://launcher-proxy:8080';
process.env.https_proxy = 'http://another:9090';
await refreshShellEnvProxyVars();
expect(process.env.HTTP_PROXY).toBe('http://launcher-proxy:8080');
expect(process.env.https_proxy).toBe('http://another:9090');
});
test('does not invoke the shell subprocess on Windows', async () => {
// A shell profile that would export a proxy if it were consulted — it must not be.
mockShellEnvResult = { http_proxy: 'http://should-not-appear:8080' };
await refreshShellEnvProxyVars();
// mockProxyVarsSeenByShell is only populated when the shell-env mock is actually called.
// Empty means we never invoked shellEnv() — the Windows early-return short-circuited.
expect(mockProxyVarsSeenByShell).toEqual({});
expect(process.env.http_proxy).toBeUndefined();
});
});

View File

@@ -6,8 +6,40 @@
import path from 'path';
const fetchShellEnv = async (): Promise<Record<string, string>> => {
export const PROXY_ENV_KEYS = [
'http_proxy',
'HTTP_PROXY',
'https_proxy',
'HTTPS_PROXY',
'no_proxy',
'NO_PROXY',
'all_proxy',
'ALL_PROXY'
] as const;
const TIMEOUT = Symbol('shell-env-timeout');
const TIMEOUT_MS = 60_000;
/**
* Races a promise against a timeout. Resolves to the TIMEOUT symbol if the
* timeout wins, so a misconfigured shell can't hang the caller indefinitely.
*/
const withTimeout = async <T>(promise: Promise<T>, timeoutMs: number): Promise<T | typeof TIMEOUT> => {
let timeoutId: ReturnType<typeof setTimeout>;
const timeoutPromise = new Promise<typeof TIMEOUT>((resolve) => {
timeoutId = setTimeout(() => resolve(TIMEOUT), timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timeoutId!);
}
};
const fetchShellEnv = async (): Promise<Record<string, string> | null> => {
// Windows handles environment variables differently - skip
// everything related to windows proxy settings is handled by the system proxy resolver i.e getSystemProxy()
if (process.platform === 'win32') {
return {};
}
@@ -18,7 +50,7 @@ const fetchShellEnv = async (): Promise<Record<string, string>> => {
const env = await shellEnv();
return env;
} catch (error) {
return {};
return null;
}
};
@@ -30,6 +62,11 @@ const fetchShellEnv = async (): Promise<Record<string, string>> => {
*/
export const initializeShellEnv = async (): Promise<Record<string, string>> => {
const shellEnvVars = await fetchShellEnv();
if (shellEnvVars === null) {
return {};
}
for (const [key, value] of Object.entries(shellEnvVars)) {
if (key === 'PATH' && process.env.PATH) {
process.env.PATH = `${value}${path.delimiter}${process.env.PATH}`;
@@ -39,3 +76,53 @@ export const initializeShellEnv = async (): Promise<Record<string, string>> => {
}
return shellEnvVars;
};
/**
* Re-syncs proxy-related process.env values from the user's shell configuration.
* Used when refreshing system proxy settings without restarting the app.
*
* @returns The fetched shell environment variables
*/
export const refreshShellEnvProxyVars = async (): Promise<Record<string, string>> => {
// Windows handles environment variables differently - skip
// everything related to windows proxy settings is handled by the
// system proxy resolver i.e getSystemProxy()
if (process.platform === 'win32') {
return {};
}
// Snapshot and clear stale proxy vars first so shell-env does not inherit them
// into the login shell subprocess (removed .zshrc exports would otherwise persist).
const snapshot: Record<string, string | undefined> = {};
for (const key of PROXY_ENV_KEYS) {
snapshot[key] = process.env[key];
delete process.env[key];
}
const restoreSnapshot = () => {
for (const key of PROXY_ENV_KEYS) {
if (snapshot[key] !== undefined) {
process.env[key] = snapshot[key] as string;
}
}
};
// Race the shell-env subprocess against a 60s timeout so a misconfigured shell
// can't hang the refresh indefinitely.
const result = await withTimeout(fetchShellEnv(), TIMEOUT_MS);
if (result === TIMEOUT || result === null) {
// Timed out — restore prior values rather than leave the user unproxied.
restoreSnapshot();
return {};
}
for (const key of PROXY_ENV_KEYS) {
const value = result[key];
if (value) {
process.env[key] = value;
}
}
return result;
};