Repository: Redocly/redocly-cli
Pull request #1461 (2024-02-29 17:41:21)

fix: fix issues with https-agent-proxy for cli and openapi-core packages
## What/Why/How?
Adding https-proxy-agent package to fix issues with requests when using CLI behind proxy. Create better mechanism for handling `env.REDOCLY_DOMAIN` usage in few modules. 

Now if you need to run the CLI tool behind a proxy, you can use the HTTP_PROXY and HTTPS_PROXY environment variables to configure the proxy settings.

`https-proxy-agent` package was used for `node-fetch` requests. Now we pass agent for every `fetchWithTimeout` request, as well as other regular `fetch` request.

This is a rollback of a rollback https://github.com/Redocly/redocly-cli/pull/1458 
After encountering problems with some of the packages from `https-proxy-agent` dependencies, we had to ignore this package for browser environment, by modyfing `packages/core/package.json` like so:
```
  "browser": {
    "https-proxy-agent": false
  },
```

## Reference
Resolves https://github.com/Redocly/redocly-cli/issues/1404
Fixes https://github.com/Redocly/redocly-cli/issues/529
Fixes https://github.com/Redocly/redocly-cli/issues/1146
Resolves https://github.com/Redocly/redocly-cli/pull/1433

## Testing
Fixed unit tests. Tested with few CLI commands: `push`, `lint` etc.

## Screenshots (optional)
n/a

## Has code been changed?
Yes

## Check yourself

- [x] Code is linted
- [x] Tested with redoc/reference-docs/workflows (internal)
- [x] All new/updated code is covered with tests

## Security

- [x] Security impact of change has been considered
- [x] Code follows company security practices and guidelines

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 47e2780083c5e676314f67b63ee9dcc982b25c7b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/Redocly/redocly-cli/new/fix/https-proxy-agent-issues-fix?filename=.changeset/tough-singers-grow.md&value=---%0A%22%40redocly%2Fcli%22%3A%20patch%0A%22%40redocly%2Fopenapi-core%22%3A%20patch%0A---%0A%0AFix%20issues%20with%20https-agent-proxy%20for%20cli%20and%20openapi-core%20packages%0A)


github-actions[bot]: | Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `redocly lint packages/core/src/benchmark/benches/rebilly.yaml` | 1.027 ± 0.066 | 0.987 | 1.208 | 1.01 ± 0.07 |
| `redocly-next lint packages/core/src/benchmark/benches/rebilly.yaml` | 1.012 ± 0.023 | 0.982 | 1.061 | 1.00 |

<!-- thollander/actions-comment-pull-request "latest-vs-next-comparison" -->
github-actions[bot]: <!-- jest coverage report action for options with hash 3339ae53a38c44382c0fb4491bdb282d -->

## Coverage report 



| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category   | Percentage | Covered / Total |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------- | :--------- | :-------------: |
|                                                                           🟡                                                                           | Statements | 76.95%     |    4468/5806    |
|                                                                           🟡                                                                           | Branches   | 67.05%     |    2362/3523    |
|                                                                           🟡                                                                           | Functions  | 70.41%     |     728/1034    |
|                                                                           🟡                                                                           | Lines      | 77.17%     |    4204/5448    |

<details><summary>Show new covered files 🐣</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File                                                              | Statements | Branches | Functions | Lines |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------- | :--------- | :------- | :-------- | :---- |
|                                                                           🟢                                                                           | <div title="core/src/redocly/domains.ts">`...` / domains.ts</div> | 100%       | 76.92%   | 100%      | 100%  |

</details>


<details><summary>Show files with reduced coverage 🔻</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File                                                                   | Statements                                                     | Branches                                                       | Functions                                                      | Lines                                                          |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------- | :------------------------------------------------------------- | :------------------------------------------------------------- | :------------------------------------------------------------- | :------------------------------------------------------------- |
|                                                                           🟡                                                                           | <div title="core/src/config/config.ts">`...` / config.ts</div>         | 70.32%                                                         | <div title="Base coverage is: 76.19%">77.3% (+1.11% 🔼)</div>  | <div title="Base coverage is: 68.97%">67.86% (-1.11% 🔻)</div> | 70%                                                            |
|                                                                           🟡                                                                           | <div title="core/src/redocly/index.ts">`...` / index.ts</div>          | <div title="Base coverage is: 66.67%">63.64% (-3.03% 🔻)</div> | <div title="Base coverage is: 71.43%">67.86% (-3.57% 🔻)</div> | <div title="Base coverage is: 75%">73.91% (-1.09% 🔻)</div>    | <div title="Base coverage is: 66.67%">63.64% (-3.03% 🔻)</div> |
|                                                                           🟡                                                                           | <div title="cli/src/commands/push.ts">`...` / push.ts</div>            | 73.53%                                                         | 68.46%                                                         | 54.55%                                                         | 75.93%                                                         |
|                                                                           🟡                                                                           | <div title="cli/src/cms/api/api-client.ts">`...` / api-client.ts</div> | <div title="Base coverage is: 80%">75.44% (-4.56% 🔻)</div>    | <div title="Base coverage is: 46.15%">41.38% (-4.77% 🔻)</div> | 75%                                                            | <div title="Base coverage is: 79.17%">74.55% (-4.62% 🔻)</div> |

</details>

## Test suite run success
722 tests passing in 102 suites.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from 47e2780083c5e676314f67b63ee9dcc982b25c7b</p>


Patch:
diff --git a/.changeset/wild-pets-report.md b/.changeset/wild-pets-report.md
new file mode 100644
index 00000000..69c0ebb3
--- /dev/null
+++ b/.changeset/wild-pets-report.md
@@ -0,0 +1,6 @@
+---
+"@redocly/openapi-core": patch
+"@redocly/cli": patch
+---
+
+Users can run the CLI tool behind a proxy by using `HTTP_PROXY` or `HTTPS_PROXY` environment variables to configure the proxy settings.
diff --git a/docs/installation.md b/docs/installation.md
index a008a072..8571d81f 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -78,6 +78,30 @@ To give a Docker container access to your OpenAPI description files, you need to
 docker run --rm -v $PWD:/spec redocly/cli lint openapi.yaml
 ```
 
+## Run CLI behind a proxy
+
+If you need to run the CLI tool behind a proxy, you can use the `HTTP_PROXY` and `HTTPS_PROXY` environment variables to configure the proxy settings. These environment variables are commonly used to specify the proxy server for HTTP and HTTPS traffic, respectively.
+
+### Set up Proxy Environment Variables
+
+Before running the CLI behind a proxy, make sure to set the appropriate proxy environment variables. Open a terminal and use the following commands:
+
+```bash
+# For HTTP proxy
+export HTTP_PROXY=http://your-http-proxy-server:port
+
+# For HTTPS proxy
+export HTTPS_PROXY=http://your-https-proxy-server:port
+```
+
+### Use Environment Variables with CLI Commands
+
+You can also directly include the proxy environment variables in the command itself. For example:
+
+```bash
+HTTPS_PROXY=https://your-https-proxy-server:port redocly lint --extends minimal openapi.yaml
+```
+
 ## Next steps
 
 - Set up [autocomplete for Redocly CLI](./guides/autocomplete.md).
diff --git a/package-lock.json b/package-lock.json
index 347c878d..cd6e6cd2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12945,6 +12945,7 @@
       "dependencies": {
         "@redocly/ajv": "^8.11.0",
         "colorette": "^1.2.0",
+        "https-proxy-agent": "^7.0.4",
         "js-levenshtein": "^1.1.6",
         "js-yaml": "^4.1.0",
         "lodash.isequal": "^4.5.0",
@@ -12967,6 +12968,29 @@
         "node": ">=14.19.0",
         "npm": ">=7.0.0"
       }
+    },
+    "packages/core/node_modules/agent-base": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
+      "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
+      "dependencies": {
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "packages/core/node_modules/https-proxy-agent": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
+      "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
+      "dependencies": {
+        "agent-base": "^7.0.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
     }
   },
   "dependencies": {
@@ -15610,6 +15634,7 @@
         "@types/node-fetch": "^2.5.7",
         "@types/pluralize": "^0.0.29",
         "colorette": "^1.2.0",
+        "https-proxy-agent": "^7.0.4",
         "js-levenshtein": "^1.1.6",
         "js-yaml": "^4.1.0",
         "lodash.isequal": "^4.5.0",
@@ -15618,6 +15643,25 @@
         "pluralize": "^8.0.0",
         "typescript": "^5.2.2",
         "yaml-ast-parser": "0.0.43"
+      },
+      "dependencies": {
+        "agent-base": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
+          "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
+          "requires": {
+            "debug": "^4.3.4"
+          }
+        },
+        "https-proxy-agent": {
+          "version": "7.0.4",
+          "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
+          "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
+          "requires": {
+            "agent-base": "^7.0.2",
+            "debug": "4"
+          }
+        }
       }
     },
     "@sinclair/typebox": {
diff --git a/packages/cli/src/__mocks__/@redocly/openapi-core.ts b/packages/cli/src/__mocks__/@redocly/openapi-core.ts
index 29dc9642..e89a8d75 100644
--- a/packages/cli/src/__mocks__/@redocly/openapi-core.ts
+++ b/packages/cli/src/__mocks__/@redocly/openapi-core.ts
@@ -21,6 +21,7 @@ export const __redoclyClient = {
 export const RedoclyClient = jest.fn(() => __redoclyClient);
 export const loadConfig = jest.fn(() => ConfigFixture);
 export const getMergedConfig = jest.fn();
+export const getProxyAgent = jest.fn();
 export const lint = jest.fn();
 export const bundle = jest.fn(() => ({ bundle: { parsed: null }, problems: null }));
 export const getTotals = jest.fn(() => ({ errors: 0 }));
diff --git a/packages/cli/src/cms/api/api-client.ts b/packages/cli/src/cms/api/api-client.ts
index 671da6c2..4d9b5b20 100644
--- a/packages/cli/src/cms/api/api-client.ts
+++ b/packages/cli/src/cms/api/api-client.ts
@@ -1,7 +1,7 @@
 import fetchWithTimeout from '../../utils/fetch-with-timeout';
 import fetch from 'node-fetch';
 import * as FormData from 'form-data';
-
+import { getProxyAgent } from '@redocly/openapi-core';
 import type { Response } from 'node-fetch';
 import type { ReadStream } from 'fs';
 import type {
@@ -25,7 +25,7 @@ class RemotesApiClient {
   }
 
   async getDefaultBranch(organizationId: string, projectId: string) {
-    const response = await fetch(
+    const response = await fetchWithTimeout(
       `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/source`,
       {
         method: 'GET',
@@ -36,6 +36,10 @@ class RemotesApiClient {
       }
     );
 
+    if (!response) {
+      throw new Error(`Failed to get default branch.`);
+    }
+
     try {
       const source = await this.getParsedResponse<ProjectSourceResponse>(response);
 
@@ -53,7 +57,7 @@ class RemotesApiClient {
       mountBranchName: string;
     }
   ): Promise<UpsertRemoteResponse> {
-    const response = await fetch(
+    const response = await fetchWithTimeout(
       `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes`,
       {
         method: 'POST',
@@ -70,6 +74,10 @@ class RemotesApiClient {
       }
     );
 
+    if (!response) {
+      throw new Error(`Failed to upsert.`);
+    }
+
     try {
       return await this.getParsedResponse<UpsertRemoteResponse>(response);
     } catch (err) {
@@ -110,6 +118,7 @@ class RemotesApiClient {
           Authorization: `Bearer ${this.apiKey}`,
         },
         body: formData,
+        agent: getProxyAgent(),
       }
     );
 
@@ -121,7 +130,7 @@ class RemotesApiClient {
   }
 
   async getRemotesList(organizationId: string, projectId: string, mountPath: string) {
-    const response = await fetch(
+    const response = await fetchWithTimeout(
       `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes?filter=mountPath:/${mountPath}/`,
       {
         method: 'GET',
@@ -132,6 +141,10 @@ class RemotesApiClient {
       }
     );
 
+    if (!response) {
+      throw new Error(`Failed to get remotes list.`);
+    }
+
     try {
       return await this.getParsedResponse<ListRemotesResponse>(response);
     } catch (err) {
@@ -160,7 +173,7 @@ class RemotesApiClient {
     );
 
     if (!response) {
-      throw new Error(`Failed to get push status: Time is up`);
+      throw new Error(`Failed to get push status.`);
     }
 
     try {
diff --git a/packages/cli/src/commands/push.ts b/packages/cli/src/commands/push.ts
index 1be48c16..7ebd3c87 100644
--- a/packages/cli/src/commands/push.ts
+++ b/packages/cli/src/commands/push.ts
@@ -14,6 +14,7 @@ import {
   slash,
   Region,
   getMergedConfig,
+  getProxyAgent,
 } from '@redocly/openapi-core';
 import {
   exitWithError,
@@ -62,8 +63,12 @@ export async function handlePush(argv: PushOptions, config: Config): Promise<voi
   const client = new RedoclyClient(config.region);
   const isAuthorized = await client.isAuthorizedWithRedoclyByRegion();
   if (!isAuthorized) {
-    const clientToken = await promptClientToken(client.domain);
-    await client.login(clientToken);
+    try {
+      const clientToken = await promptClientToken(client.domain);
+      await client.login(clientToken);
+    } catch (e) {
+      exitWithError(e);
+    }
   }
 
   const startedAt = performance.now();
@@ -439,6 +444,7 @@ function uploadFileToS3(url: string, filePathOrBuffer: string | Buffer) {
     typeof filePathOrBuffer === 'string'
       ? fs.statSync(filePathOrBuffer).size
       : filePathOrBuffer.byteLength;
+
   const readStream =
     typeof filePathOrBuffer === 'string' ? fs.createReadStream(filePathOrBuffer) : filePathOrBuffer;
 
@@ -448,5 +454,6 @@ function uploadFileToS3(url: string, filePathOrBuffer: string | Buffer) {
       'Content-Length': fileSizeInBytes.toString(),
     },
     body: readStream,
+    agent: getProxyAgent(),
   });
 }
diff --git a/packages/cli/src/utils/fetch-with-timeout.ts b/packages/cli/src/utils/fetch-with-timeout.ts
index 9a373e33..82bd29b3 100644
--- a/packages/cli/src/utils/fetch-with-timeout.ts
+++ b/packages/cli/src/utils/fetch-with-timeout.ts
@@ -1,5 +1,6 @@
 import nodeFetch from 'node-fetch';
 import AbortController from 'abort-controller';
+import { getProxyAgent } from '@redocly/openapi-core';
 
 const TIMEOUT = 3000;
 
@@ -10,7 +11,11 @@ export default async (url: string, options = {}) => {
       controller.abort();
     }, TIMEOUT);
 
-    const res = await nodeFetch(url, { signal: controller.signal, ...options });
+    const res = await nodeFetch(url, {
+      signal: controller.signal,
+      ...options,
+      agent: getProxyAgent(),
+    });
     clearTimeout(timeout);
     return res;
   } catch (e) {
diff --git a/packages/core/package.json b/packages/core/package.json
index 48ad6afe..46b578f9 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -18,7 +18,8 @@
     "path": "path-browserify",
     "os": false,
     "node-fetch": false,
-    "colorette": false
+    "colorette": false,
+    "https-proxy-agent": false
   },
   "homepage": "https://github.com/Redocly/redocly-cli",
   "keywords": [
@@ -36,6 +37,7 @@
   "dependencies": {
     "@redocly/ajv": "^8.11.0",
     "colorette": "^1.2.0",
+    "https-proxy-agent": "^7.0.4",
     "js-levenshtein": "^1.1.6",
     "js-yaml": "^4.1.0",
     "lodash.isequal": "^4.5.0",
diff --git a/packages/core/src/bundle.ts b/packages/core/src/bundle.ts
index 0c80f6e7..2c6a4580 100755
--- a/packages/core/src/bundle.ts
+++ b/packages/core/src/bundle.ts
@@ -14,7 +14,7 @@ import { isAbsoluteUrl, isRef, Location, refBaseName } from './ref-utils';
 import { initRules } from './config/rules';
 import { reportUnresolvedRef } from './rules/no-unresolved-refs';
 import { isPlainObject, isTruthy } from './utils';
-import { isRedoclyRegistryURL } from './redocly';
+import { isRedoclyRegistryURL } from './redocly/domains';
 import { RemoveUnusedComponents as RemoveUnusedComponentsOas2 } from './decorators/oas2/remove-unused-components';
 import { RemoveUnusedComponents as RemoveUnusedComponentsOas3 } from './decorators/oas3/remove-unused-components';
 import { ConfigTypes } from './types/redocly-yaml';
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 3728372f..cda45c6c 100755
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -10,7 +10,7 @@ import {
   Oas3RuleSet,
   Async2RuleSet,
 } from '../oas-types';
-import { isBrowser, env } from '../env';
+import { isBrowser } from '../env';
 
 import type { NodeType } from '../types';
 import type {
@@ -35,25 +35,6 @@ const IGNORE_BANNER =
   `# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API.\n` +
   `# See https://redoc.ly/docs/cli/ for more information.\n`;
 
-export const DEFAULT_REGION = 'us';
-
-function getDomains() {
-  const domains: { [region in Region]: string } = {
-    us: 'redocly.com',
-    eu: 'eu.redocly.com',
-  };
-
-  // FIXME: temporary fix for our lab environments
-  const domain = env.REDOCLY_DOMAIN;
-  if (domain?.endsWith('.redocly.host')) {
-    domains[domain.split('.')[0] as Region] = domain;
-  }
-  if (domain === 'redoc.online') {
-    domains[domain as Region] = domain;
-  }
-  return domains;
-}
-
 function getIgnoreFilePath(configFile?: string): string | undefined {
   if (configFile) {
     return doesYamlFileExist(configFile)
@@ -64,9 +45,6 @@ function getIgnoreFilePath(configFile?: string): string | undefined {
   }
 }
 
-export const DOMAINS = getDomains();
-export const AVAILABLE_REGIONS = Object.keys(DOMAINS) as Region[];
-
 export class StyleguideConfig {
   plugins: Plugin[];
   ignore: Record<string, Record<string, Set<string>>> = {};
diff --git a/packages/core/src/config/load.ts b/packages/core/src/config/load.ts
index c7ba384f..0ff28963 100644
--- a/packages/core/src/config/load.ts
+++ b/packages/core/src/config/load.ts
@@ -3,7 +3,7 @@ import * as path from 'path';
 import { RedoclyClient } from '../redocly';
 import { isEmptyObject, doesYamlFileExist } from '../utils';
 import { parseYaml } from '../js-yaml';
-import { Config, DOMAINS } from './config';
+import { Config } from './config';
 import { ConfigValidationError, transformConfig } from './utils';
 import { resolveConfig, resolveConfigFileAndRefs } from './config-resolvers';
 import { bundleConfig } from '../bundle';
@@ -12,6 +12,7 @@ import type { Document } from '../resolve';
 import type { RegionalTokenWithValidity } from '../redocly/redocly-client-types';
 import type { RawConfig, RawUniversalConfig, Region } from './types';
 import type { BaseResolver, ResolvedRefMap } from '../resolve';
+import { DOMAINS } from '../redocly/domains';
 
 async function addConfigMetadata({
   rawConfig,
diff --git a/packages/core/src/decorators/common/registry-dependencies.ts b/packages/core/src/decorators/common/registry-dependencies.ts
index 2e58f0bb..007361b9 100644
--- a/packages/core/src/decorators/common/registry-dependencies.ts
+++ b/packages/core/src/decorators/common/registry-dependencies.ts
@@ -1,5 +1,5 @@
 import { UserContext } from '../../walk';
-import { isRedoclyRegistryURL } from '../../redocly';
+import { isRedoclyRegistryURL } from '../../redocly/domains';
 
 import { Oas3Decorator, Oas2Decorator } from '../../visitors';
 
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index cfdf870d..d500311a 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -1,4 +1,11 @@
-export { BundleOutputFormat, readFileFromUrl, slash, doesYamlFileExist, isTruthy } from './utils';
+export {
+  BundleOutputFormat,
+  readFileFromUrl,
+  slash,
+  doesYamlFileExist,
+  isTruthy,
+  getProxyAgent,
+} from './utils';
 export { Oas3_1Types } from './types/oas3_1';
 export { Oas3Types } from './types/oas3';
 export { Oas2Types } from './types/oas2';
@@ -41,7 +48,9 @@ export {
   ResolvedApi,
 } from './config';
 
-export { RedoclyClient, isRedoclyRegistryURL } from './redocly';
+export { RedoclyClient } from './redocly';
+
+export * from './redocly/domains';
 
 export {
   Source,
diff --git a/packages/core/src/redocly/domains.ts b/packages/core/src/redocly/domains.ts
new file mode 100644
index 00000000..3fa6856f
--- /dev/null
+++ b/packages/core/src/redocly/domains.ts
@@ -0,0 +1,48 @@
+import { Region } from '../config/types';
+
+let redoclyDomain = 'redocly.com';
+
+export const DEFAULT_REGION = 'us';
+
+export const DOMAINS = getDomains();
+export const AVAILABLE_REGIONS = Object.keys(DOMAINS) as Region[];
+
+export function getDomains() {
+  const domains: { [region in Region]: string } = {
+    us: 'redocly.com',
+    eu: 'eu.redocly.com',
+  };
+
+  // FIXME: temporary fix for our lab environments
+  const domain = redoclyDomain;
+  if (domain?.endsWith('.redocly.host')) {
+    domains[domain.split('.')[0] as Region] = domain;
+  }
+  if (domain === 'redoc.online') {
+    domains[domain as Region] = domain;
+  }
+  return domains;
+}
+
+export function setRedoclyDomain(domain: string) {
+  redoclyDomain = domain;
+}
+
+export function getRedoclyDomain(): string {
+  return redoclyDomain;
+}
+
+export function isRedoclyRegistryURL(link: string): boolean {
+  const domain = getRedoclyDomain() || DOMAINS[DEFAULT_REGION];
+
+  const legacyDomain = domain === 'redocly.com' ? 'redoc.ly' : domain;
+
+  if (
+    !link.startsWith(`https://api.${domain}/registry/`) &&
+    !link.startsWith(`https://api.${legacyDomain}/registry/`)
+  ) {
+    return false;
+  }
+
+  return true;
+}
diff --git a/packages/core/src/redocly/index.ts b/packages/core/src/redocly/index.ts
index 4b1de8b6..df28bdd6 100644
--- a/packages/core/src/redocly/index.ts
+++ b/packages/core/src/redocly/index.ts
@@ -2,13 +2,19 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
 import { resolve } from 'path';
 import { homedir } from 'os';
 import { RegistryApi } from './registry-api';
-import { DEFAULT_REGION, DOMAINS, AVAILABLE_REGIONS } from '../config/config';
 import { env } from '../env';
 import { RegionalToken, RegionalTokenWithValidity } from './redocly-client-types';
 import { isNotEmptyObject } from '../utils';
 import { colorize } from '../logger';
 
 import type { AccessTokens, Region } from '../config/types';
+import {
+  AVAILABLE_REGIONS,
+  DEFAULT_REGION,
+  DOMAINS,
+  getRedoclyDomain,
+  setRedoclyDomain,
+} from './domains';
 
 export const TOKEN_FILENAME = '.redocly-config.json';
 
@@ -23,7 +29,7 @@ export class RedoclyClient {
     this.loadTokens();
     this.domain = region ? DOMAINS[region] : env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION];
 
-    env.REDOCLY_DOMAIN = this.domain; // isRedoclyRegistryURL depends on the value to be set
+    setRedoclyDomain(this.domain);
     this.registryApi = new RegistryApi(this.accessTokens, this.region);
   }
 
@@ -36,9 +42,9 @@ export class RedoclyClient {
       );
     }
 
-    if (env.REDOCLY_DOMAIN) {
+    if (getRedoclyDomain()) {
       return (AVAILABLE_REGIONS.find(
-        (region) => DOMAINS[region as Region] === env.REDOCLY_DOMAIN
+        (region) => DOMAINS[region as Region] === getRedoclyDomain()
       ) || DEFAULT_REGION) as Region;
     }
     return region || DEFAULT_REGION;
@@ -96,7 +102,7 @@ export class RedoclyClient {
     const allTokens = this.getAllTokens();
 
     const verifiedTokens = await Promise.allSettled(
-      allTokens.map(({ token, region }) => this.verifyToken(token, region))
+      allTokens.map(({ token }) => this.verifyToken(token))
     );
 
     return allTokens
@@ -120,7 +126,7 @@ export class RedoclyClient {
     }
 
     try {
-      await this.verifyToken(accessToken, this.region);
+      await this.verifyToken(accessToken);
 
       return true;
     } catch (err) {
@@ -138,17 +144,16 @@ export class RedoclyClient {
 
   async verifyToken(
     accessToken: string,
-    region: Region,
     verbose: boolean = false
   ): Promise<{ viewerId: string; organizations: string[] }> {
-    return this.registryApi.authStatus(accessToken, region, verbose);
+    return this.registryApi.authStatus(accessToken, verbose);
   }
 
   async login(accessToken: string, verbose: boolean = false) {
     const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
 
     try {
-      await this.verifyToken(accessToken, this.region, verbose);
+      await this.verifyToken(accessToken, verbose);
     } catch (err) {
       throw new Error('Authorization failed. Please check if you entered a valid API key.');
     }
@@ -170,18 +175,3 @@ export class RedoclyClient {
     }
   }
 }
-
-export function isRedoclyRegistryURL(link: string): boolean {
-  const domain = env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION];
-
-  const legacyDomain = domain === 'redocly.com' ? 'redoc.ly' : domain;
-
-  if (
-    !link.startsWith(`https://api.${domain}/registry/`) &&
-    !link.startsWith(`https://api.${legacyDomain}/registry/`)
-  ) {
-    return false;
-  }
-
-  return true;
-}
diff --git a/packages/core/src/redocly/registry-api.ts b/packages/core/src/redocly/registry-api.ts
index 63ce39a7..5d77c6a4 100644
--- a/packages/core/src/redocly/registry-api.ts
+++ b/packages/core/src/redocly/registry-api.ts
@@ -6,8 +6,8 @@ import type {
   PushApiParams,
 } from './registry-api-types';
 import type { AccessTokens, Region } from '../config/types';
-import { DEFAULT_REGION, DOMAINS } from '../config/config';
-import { isNotEmptyObject } from '../utils';
+import { getProxyAgent, isNotEmptyObject } from '../utils';
+import { getRedoclyDomain } from './domains';
 
 const version = require('../../package.json').version;
 
@@ -18,8 +18,8 @@ export class RegistryApi {
     return isNotEmptyObject(this.accessTokens) && this.accessTokens[this.region];
   }
 
-  getBaseUrl(region: Region = DEFAULT_REGION) {
-    return `https://api.${DOMAINS[region]}/registry`;
+  getBaseUrl() {
+    return `https://api.${getRedoclyDomain()}/registry`;
   }
 
   setAccessTokens(accessTokens: AccessTokens) {
@@ -27,7 +27,7 @@ export class RegistryApi {
     return this;
   }
 
-  private async request(path = '', options: RequestInit = {}, region?: Region) {
+  private async request(path = '', options: RequestInit = {}) {
     const currentCommand =
       typeof process !== 'undefined' ? process.env?.REDOCLY_CLI_COMMAND || '' : '';
     const redoclyEnv = typeof process !== 'undefined' ? process.env?.REDOCLY_ENVIRONMENT || '' : '';
@@ -42,8 +42,8 @@ export class RegistryApi {
     }
 
     const response = await fetch(
-      `${this.getBaseUrl(region)}${path}`,
-      Object.assign({}, options, { headers })
+      `${this.getBaseUrl()}${path}`,
+      Object.assign({}, options, { headers, agent: getProxyAgent() })
     );
 
     if (response.status === 401) {
@@ -60,11 +60,10 @@ export class RegistryApi {
 
   async authStatus(
     accessToken: string,
-    region: Region,
     verbose = false
   ): Promise<{ viewerId: string; organizations: string[] }> {
     try {
-      const response = await this.request('', { headers: { authorization: accessToken } }, region);
+      const response = await this.request('', { headers: { authorization: accessToken } });
 
       return await response.json();
     } catch (error) {
@@ -97,8 +96,7 @@ export class RegistryApi {
           filename,
           isUpsert,
         }),
-      },
-      this.region
+      }
     );
 
     if (response.ok) {
@@ -120,26 +118,22 @@ export class RegistryApi {
     batchId,
     batchSize,
   }: PushApiParams) {
-    const response = await this.request(
-      `/${organizationId}/${name}/${version}`,
-      {
-        method: 'PUT',
-        headers: {
-          'content-type': 'application/json',
-          authorization: this.accessToken,
-        } as HeadersInit,
-        body: JSON.stringify({
-          rootFilePath,
-          filePaths,
-          branch,
-          isUpsert,
-          isPublic,
-          batchId,
-          batchSize,
-        }),
-      },
-      this.region
-    );
+    const response = await this.request(`/${organizationId}/${name}/${version}`, {
+      method: 'PUT',
+      headers: {
+        'content-type': 'application/json',
+        authorization: this.accessToken,
+      } as HeadersInit,
+      body: JSON.stringify({
+        rootFilePath,
+        filePaths,
+        branch,
+        isUpsert,
+        isPublic,
+        batchId,
+        batchSize,
+      }),
+    });
 
     if (response.ok) {
       return;
diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts
index 33034e14..a4b195a3 100644
--- a/packages/core/src/utils.ts
+++ b/packages/core/src/utils.ts
@@ -5,9 +5,10 @@ import fetch from 'node-fetch';
 import * as pluralize from 'pluralize';
 import { parseYaml } from './js-yaml';
 import { UserContext } from './walk';
-import { HttpResolveConfig } from './config';
 import { env } from './env';
 import { logger, colorize } from './logger';
+import { HttpResolveConfig } from './config';
+import { HttpsProxyAgent } from 'https-proxy-agent';
 
 export { parseYaml, stringifyYaml } from './js-yaml';
 
@@ -274,3 +275,8 @@ export function nextTick() {
 function getUpdatedFieldName(updatedField: string, updatedObject?: string) {
   return `${typeof updatedObject !== 'undefined' ? `${updatedObject}.` : ''}${updatedField}`;
 }
+
+export function getProxyAgent() {
+  const proxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
+  return proxy ? new HttpsProxyAgent(proxy) : undefined;
+}


Test patch:
diff --git a/packages/cli/src/cms/api/__tests__/api.client.test.ts b/packages/cli/src/cms/api/__tests__/api.client.test.ts
index f654ad70..cb7a2c13 100644
--- a/packages/cli/src/cms/api/__tests__/api.client.test.ts
+++ b/packages/cli/src/cms/api/__tests__/api.client.test.ts
@@ -42,6 +42,7 @@ describe('ApiClient', () => {
             'Content-Type': 'application/json',
             Authorization: `Bearer ${testToken}`,
           },
+          signal: expect.any(Object),
         }
       );
 
@@ -122,6 +123,8 @@ describe('ApiClient', () => {
             type: 'CICD',
             autoMerge: true,
           }),
+          signal: expect.any(Object),
+          agent: undefined,
         }
       );
 
diff --git a/packages/core/src/redocly/__tests__/domains.test.ts b/packages/core/src/redocly/__tests__/domains.test.ts
new file mode 100644
index 00000000..848eb0a9
--- /dev/null
+++ b/packages/core/src/redocly/__tests__/domains.test.ts
@@ -0,0 +1,52 @@
+import { RedoclyClient } from '../../index';
+import { setRedoclyDomain, getRedoclyDomain, getDomains, AVAILABLE_REGIONS } from '../domains';
+
+describe('domains', () => {
+  const REDOCLY_DOMAIN_US = 'redocly.com';
+  const TEST_DOMAIN = 'redoclyDomain.com';
+  const TEST_LAB_DOMAIN = 'lab.redocly.host';
+  const TEST_REDOC_ONLINE_DOMAIN = 'redoc.online';
+
+  afterEach(() => {
+    delete process.env.REDOCLY_DOMAIN;
+    setRedoclyDomain('');
+  });
+
+  it('should resolve the US domain by default', () => {
+    expect(getRedoclyDomain()).toBe(REDOCLY_DOMAIN_US);
+  });
+
+  it('should resolve the US and EU regions by default', () => {
+    expect(AVAILABLE_REGIONS).toStrictEqual(['us', 'eu']);
+  });
+
+  it('should resolve the specified domain if used with setter', () => {
+    setRedoclyDomain(TEST_DOMAIN);
+    expect(getRedoclyDomain()).toBe(TEST_DOMAIN);
+  });
+
+  it('should resolve the specified domain provided in environmental variable, after initializing RedoclyClient', () => {
+    process.env.REDOCLY_DOMAIN = TEST_DOMAIN;
+    const client = new RedoclyClient();
+    expect(getRedoclyDomain()).toBe(TEST_DOMAIN);
+    expect(client.domain).toBe(TEST_DOMAIN);
+  });
+
+  it('should return correct object when redocly domain is set to lab env', () => {
+    setRedoclyDomain(TEST_LAB_DOMAIN);
+    const domains = getDomains();
+    expect(domains).toEqual({ us: 'redocly.com', eu: 'eu.redocly.com', lab: 'lab.redocly.host' });
+    expect(getRedoclyDomain()).toBe(TEST_LAB_DOMAIN);
+  });
+
+  it('should return correct object when redocly domain is set to redoc.online env', () => {
+    setRedoclyDomain(TEST_REDOC_ONLINE_DOMAIN);
+    const domains = getDomains();
+    expect(domains).toEqual({
+      us: 'redocly.com',
+      eu: 'eu.redocly.com',
+      'redoc.online': 'redoc.online',
+    });
+    expect(getRedoclyDomain()).toBe(TEST_REDOC_ONLINE_DOMAIN);
+  });
+});
diff --git a/packages/core/src/redocly/__tests__/redocly-client.test.ts b/packages/core/src/redocly/__tests__/redocly-client.test.ts
index 0af0aaf5..0e19752d 100644
--- a/packages/core/src/redocly/__tests__/redocly-client.test.ts
+++ b/packages/core/src/redocly/__tests__/redocly-client.test.ts
@@ -1,3 +1,4 @@
+import { setRedoclyDomain } from '../domains';
 import { RedoclyClient } from '../index';
 
 jest.mock('node-fetch', () => ({
@@ -16,6 +17,7 @@ describe('RedoclyClient', () => {
 
   afterEach(() => {
     delete process.env.REDOCLY_DOMAIN;
+    setRedoclyDomain('');
   });
 
   it('should resolve the US domain by default', () => {
@@ -40,19 +42,19 @@ describe('RedoclyClient', () => {
   });
 
   it('should resolve domain by EU region prioritizing flag over env variable', () => {
-    process.env.REDOCLY_DOMAIN = testRedoclyDomain;
+    setRedoclyDomain(testRedoclyDomain);
     const client = new RedoclyClient('eu');
     expect(client.domain).toBe(REDOCLY_DOMAIN_EU);
   });
 
   it('should resolve domain by US region prioritizing flag over env variable', () => {
-    process.env.REDOCLY_DOMAIN = testRedoclyDomain;
+    setRedoclyDomain(testRedoclyDomain);
     const client = new RedoclyClient('us');
     expect(client.domain).toBe(REDOCLY_DOMAIN_US);
   });
 
   it('should resolve domain by US region when REDOCLY_DOMAIN consists EU domain', () => {
-    process.env.REDOCLY_DOMAIN = REDOCLY_DOMAIN_EU;
+    setRedoclyDomain(REDOCLY_DOMAIN_EU);
     const client = new RedoclyClient();
     expect(client.getRegion()).toBe('eu');
   });