diff --git a/.changeset/proxy-tls-support.md b/.changeset/proxy-tls-support.md new file mode 100644 index 0000000..b403c71 --- /dev/null +++ b/.changeset/proxy-tls-support.md @@ -0,0 +1,18 @@ +--- +"@codacy/codacy-cloud-cli": minor +--- + +Add HTTP/HTTPS proxy and TLS support, so the CLI works behind a corporate proxy (#40). + +Every command now honors the standard environment variables: + +- `HTTPS_PROXY` / `HTTP_PROXY` (and lowercase) — proxy URL per scheme; a bare `host:port` is accepted +- `NO_PROXY` / `no_proxy` — hosts that bypass the proxy (`*`, `.suffix`), matched per request +- `SSL_CERT_FILE` / `NODE_EXTRA_CA_CERTS` — PEM CA bundle for a TLS-intercepting proxy +- `CODACY_CLI_INSECURE` — disable TLS verification as a last resort (warns on stderr) + +These are the same variable names the Codacy Analysis CLI and the Codacy VS Code extension use, so one environment configures all of them. The implementation is the shared `configureProxy()` from `@codacy/tooling` rather than a local reimplementation, which is what keeps the behavior identical across the tools. Misconfiguration fails immediately rather than silently doing something else: an unreadable or non-PEM CA bundle reports the path instead of quietly falling back to the default trust store, and a malformed proxy URL reports which variable was wrong and why (with any proxy password redacted) instead of a bare `Invalid URL`. + +Nothing changes when no proxy variable is set — the proxy dependency is loaded lazily, so an unproxied run has no measurable overhead. + +Thanks to @rattalur for reporting the gap and for the initial implementation in #39. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52a9b03..d4e5165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,30 @@ jobs: - name: Build run: npm run build + - name: Smoke test the built CLI + run: | + # Nothing in the test suite executes the entry point -- every command test + # builds a bare `new Command()` -- so this step is the only coverage of + # src/index.ts's top-level block, on every Node version we support. + node dist/index.js --version + + # With proxy env set, the undici dispatcher is actually constructed (the + # run above returns early, since no proxy variable is set). `--version` + # makes no request, so the unreachable proxy is never dialed. This guards + # against a proxy dependency that fails to load or construct on Node 20. + HTTPS_PROXY=http://127.0.0.1:9 node dist/index.js --version + + # A misconfigured CA bundle must fail loudly rather than silently fall + # back to the system trust store. + if out=$(SSL_CERT_FILE=/nonexistent/ca.pem node dist/index.js --version 2>&1); then + echo "::error::Expected a non-zero exit for an unreadable SSL_CERT_FILE" + exit 1 + fi + case "$out" in + *"Failed to read CA certificate"*) ;; + *) echo "::error::Unexpected failure output: $out"; exit 1 ;; + esac + - name: Test run: npm test diff --git a/AGENTS.md b/AGENTS.md index c934732..da5a782 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,6 +87,7 @@ codacy-cloud-cli/ - Default cadence is `POLL_INTERVAL_MS` (10s), capped at `MAX_WAIT_MS` (20min). - **Error handling:** Use `try/catch` with the shared `handleError()` from `src/utils/error.ts` - **API base URL:** `https://app.codacy.com/api/v3` (configured in `src/index.ts` via `OpenAPI.BASE`) +- **Proxy / TLS:** never hand-roll this. Outbound HTTP configuration is delegated to `configureProxy()` from `@codacy/tooling`, wrapped by `configureProxyFromEnv()` in `src/utils/proxy.ts` and called once at the top of `src/index.ts`. It installs a global `undici` dispatcher, so every `fetch` — the generated client and the CVE lookup alike — is covered without touching generated code. Keeping the implementation upstream is what keeps the environment contract identical to the Codacy Analysis CLI; a local reimplementation would drift. If proxy behavior needs to change, change it in `analysis-cli`'s `packages/tooling/src/proxy.ts` and bump the dependency here. - **Authentication — two token kinds.** Read `SPECS/repository-tokens.md` before touching auth or adding a command. - An **account token** (`api-token` header) reaches everything its owner can see. - A **repository token** (`project-token` header) is scoped to one repository. It is accepted only on a fixed whitelist of 13 operations; everywhere else Codacy rejects it as if no token had been sent. @@ -240,6 +241,11 @@ When completing work, agents **must** update relevant documentation: |---|---|---| | `CODACY_API_TOKEN` | One of the two | Account API token. Get it from Codacy > Account > API Tokens | | `CODACY_PROJECT_TOKEN` | One of the two | Repository (project) token, scoped to one repository. Get it from Codacy > Repository > Settings > Integrations > Project API token. **Outranks `CODACY_API_TOKEN`** — see `SPECS/repository-tokens.md` | +| `HTTPS_PROXY` / `HTTP_PROXY` | No | Proxy URL per scheme (lowercase also honored). Resolved by `@codacy/tooling`'s `configureProxy()`, called once from `src/index.ts` via `configureProxyFromEnv()` | +| `NO_PROXY` / `no_proxy` | No | Comma-separated hosts that bypass the proxy (`*`, `.suffix`), matched **per request** — not once at startup | +| `SSL_CERT_FILE` / `NODE_EXTRA_CA_CERTS` | No | PEM CA bundle for a TLS-intercepting proxy. **Replaces** the default trust store; unreadable or non-PEM is fatal by design | +| `CODACY_CLI_INSECURE` | No | Disable TLS verification (also `NODE_TLS_REJECT_UNAUTHORIZED=0`). Last resort; warns on stderr | +| `CODACY_DISABLE_UPDATE_CHECK` | No | Disable the "update available" notice. Its `got` stack ignores the proxy variables above, so this is the escape hatch behind a strict proxy | ## Useful Context diff --git a/README.md b/README.md index 26efce1..8024e90 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,31 @@ An explicit `--repository-token` wins outright, so a deliberately scoped run is Passing `--repository-token` with an **empty** value is an error rather than a fallback. `--repository-token "$CODACY_PROJECT_TOKEN"` with the secret unset is a common CI mistake, and quietly falling back to an account token would run with much wider access than you asked for. An empty *environment variable*, by contrast, simply means "unset". +## Proxy and TLS + +All outbound requests honor the standard proxy environment variables — set them once and every command routes accordingly. + +| Variable | Purpose | +|---|---| +| `HTTPS_PROXY` / `HTTP_PROXY` (or lowercase) | Proxy URL for HTTPS / HTTP requests. A bare `host:port` is treated as `http://` | +| `NO_PROXY` / `no_proxy` | Comma-separated hosts that bypass the proxy (`*`, `.suffix`), matched per request | +| `SSL_CERT_FILE` / `NODE_EXTRA_CA_CERTS` | PEM CA bundle to trust, e.g. for a corporate SSL-inspection proxy | +| `CODACY_CLI_INSECURE` / `NODE_TLS_REJECT_UNAUTHORIZED=0` | Disable TLS verification (last resort; warns on stderr) | + +```bash +export HTTPS_PROXY=http://proxy.corp:8080 +export NO_PROXY=app.codacy.com,.internal +export SSL_CERT_FILE=/path/to/corporate-ca.pem # prefer trusting the CA over disabling TLS +``` + +If your proxy performs TLS interception (MITM), trust its CA rather than disabling verification. Node doesn't read the OS trust store, so requests can fail with `unable to get local issuer certificate` even when `curl -x "$HTTPS_PROXY" https://app.codacy.com/api/v3/user` against the same host succeeds — curl working while the CLI doesn't is the tell-tale sign. Ask your IT team for the bundle, or export it from your OS trust store in PEM format. + +Note that `SSL_CERT_FILE` **replaces** the default trust store rather than adding to it, the same way curl's `--cacert` does, so the bundle must contain the full chain for every host you reach — including hosts that bypass the proxy via `NO_PROXY`. A misconfigured or unreadable bundle fails fast with a clear error instead of silently falling back. + +These variable names match the Codacy Analysis CLI and the Codacy VS Code extension, so one environment drives all of them. + +> The "update available" notice uses a separate network stack that does not honor these variables. Behind a strict proxy, disable it with `CODACY_DISABLE_UPDATE_CHECK=1`. + ## Usage ```bash @@ -144,7 +169,7 @@ npm run update-api # Update the auto-generated API client ### CI/CD -- **CI**: Runs on every push to `main` and on PRs. Builds and tests across Node.js 18, 20, and 22. +- **CI**: Runs on every push to `main` and on PRs. Builds, smoke-tests the built CLI, and runs the test suite across Node.js 20 and 22. - **Release**: Uses [changesets](https://github.com/changesets/changesets) for automated versioning and npm publishing. #### Publishing a new version diff --git a/SPECS/README.md b/SPECS/README.md index 31469e4..30f6de2 100644 --- a/SPECS/README.md +++ b/SPECS/README.md @@ -86,3 +86,4 @@ _No pending tasks._ All commands implemented. | 2026-07-28 | (OD-378) New `pull-requests` (`prs`) command — the plural counterpart to `pull-request`, listing PRs for a repository with the same analysis-gated table columns as `repository`'s "Open Pull Requests" section (reuses `buildGateStatus`/`formatStandards`/`formatPrIssues`/`formatPrCoverage`/`formatDelta`). `--search-text`/`-q` and `--branch`/`-b` map to the API's `textQuery`/`targetBranch` params added in OD-376; the classification param (`search`, Merged vs. last-updated) is deliberately not exposed — different axis, out of scope. `[provider] [org] [repo]` auto-detect via `resolveRepoArgs`, paginate-to-`--limit` loop matching `findings`. Registered in `src/index.ts` (10 new tests, 516 total) | | 2026-07-30 | (OD-378, review follow-up) `pull-requests` table polish + a real data bug. **Bug:** Complexity rendered as "no data" on every PR because the API omits the flat top-level `deltaComplexity` and only returns `quality.deltaComplexity` (while still sending a top-level `deltaClonesCount`) — new shared `prQualityMetric(pr, key)` in `utils/formatting.ts` reads the nested `quality` value first and falls back to the flat field; also applied to `repository`'s Open PR table and `pull-request`'s Analysis section, which had the same bug. **Layout:** `✓` moved to the first column; metric order now matches `repositories` (issues → complexity → duplication → coverage); the Coverage column is dropped entirely when no listed PR has a coverage value (new `hasAnyPrCoverage()` — repos without coverage return `diffCoverage.cause` and no numbers on any PR); missing metric values now render as a dim `-` instead of `N/A` in `formatDelta`/`formatPrCoverage`/`formatPrIssues`, matching `formatStandards`/`formatCountCell`/`formatCoverageCell`; and a zero issue count renders as a bare `0` rather than `+0`/`-0` (`-0` read as a negative), matching what `pull-request`'s Files table and `formatDelta` already did. **JSON:** added `quality.resultReasons`/`coverage.resultReasons` (Codacy review suggestion — they drive the per-metric gate coloring, so consumers need them to see which gates passed/failed) plus the `quality.*` metric mirrors the table actually renders (23 new tests, 544 total) | | 2026-08-11 | (OD-489) Repository (project) token support. New `--repository-token ` on every command (plus `CODACY_PROJECT_TOKEN`), sent as the `project-token` header; account tokens keep `api-token`. `src/utils/auth.ts` rewritten around a `RemoteAuth` discriminated union carrying both kind and source, replacing `checkApiToken()` with `resolveAuth(this)` / `resolveAccountAuth(this, why)` / `requireAccountToken(...)` / `fetchIfAccountToken(...)`. Precedence matches `codacy-analysis` exactly — flag > `CODACY_PROJECT_TOKEN` > `CODACY_API_TOKEN` > stored login — so `vitest.config.mts` now blanks `CODACY_PROJECT_TOKEN` (it outranks the account token and is exported job-wide by the coverage reporter, so tests would otherwise depend on the developer's shell). Codacy whitelists only 13 operations for repository tokens, so `tool`/`patterns`/`pattern` work unchanged, `issues` (incl. `--overview`) and `tools --import` work, and the 9 account-only commands plus `repository`'s 6 management flags, `issues --ignore`/`--ignored`, and `tools --import --force` (only when standards exist) **fail fast before any request** with a message naming the operation, the reason, and where the token came from. `repository`'s dashboard skips the two non-whitelisted calls: the table keeps the "Open Pull Requests" header with an explanatory line, and JSON keeps `pullRequests: []` (so `jq '.pullRequests[]'` still works) plus an additive `unavailable: ["pullRequests"]` — under an account token the payload is byte-identical. Also added the long-missing `.catch()` on the PR call so an account token lacking PR access degrades instead of losing the whole dashboard, and fixed `login`'s 401 message, which told repository-token users their token was "invalid" when it is rejected by `/user` by design. New `SPECS/repository-tokens.md` (whitelist + matrix, re-verify on every `npm run update-api`) and `SPECS/missing-endpoints.md` (ranked gaps for follow-up Linear tasks) (40 new tests, 606 total) | +| 2026-09-07 | HTTP/HTTPS proxy + TLS support (issue #40). Node's global `fetch` — used by the generated client and the MITRE CVE lookup in `commands/finding.ts` — ignores `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`, so the CLI was unusable behind a corporate proxy. Rather than reimplement it, this delegates to `configureProxy()` from `@codacy/tooling` (pinned `0.1.0` → `0.23.0`, the same function `analysis-cli` calls), which installs a global `undici` dispatcher doing per-request protocol + `NO_PROXY` routing, bare `host:port` normalization, and `SSL_CERT_FILE`/`NODE_EXTRA_CA_CERTS` CA loading. New `src/utils/proxy.ts` is a ~4-line seam — `configureProxyFromEnv()` calls it and routes its deliberate fail-loud throw (unreadable/non-PEM CA bundle) into `handleError()`, giving red `Error: ` and exit 1 like every other failure here; `analysis-cli` exits 2 because it has a documented exit-code scheme, which this CLI does not. Called at the top of `src/index.ts`, above `OpenAPI.BASE` (ordering is only constrained to precede `program.parse`, since the dispatcher is resolved per request). Kept top-level rather than in the `preAction` hook so a typo'd `SSL_CERT_FILE` fails even on `--version`. Deliberately zero-argument: env is the sole input, which is what keeps parity exact. Superseded external PR #39, which hand-rolled the same feature with `undici@8.10.1` — that requires Node ≥ 22.19.0 against this package's `engines: ">=20"`, so `require("undici")` threw at module load and the CLI would not start at all on any Node 20.x; tooling's `undici@^6.21.0` supports Node ≥ 18.17. That regression passed CI, so `ci.yml` gained a smoke step running the built entry point (plain, with `HTTPS_PROXY`, and with a bad `SSL_CERT_FILE` expected to fail) — previously nothing executed `src/index.ts`, since every command test builds a bare `new Command()`. Upstream owns the proxy semantics and their 24 tests, so only the seam is tested here. Pinned exactly rather than with a caret: for a pre-1.0 package `^0.22.0` spans patches only (`>=0.22.0 <0.23.0-0`), so a caret would have bought silent patch drift against a dependency this repo has no proxy coverage for, without ever picking up a minor. Two findings from this work were fixed upstream and taken here via 0.23.0 — `undici` now loads lazily behind `configureProxy`'s early-out (an unproxied `--version` went from +27 ms to +0 ms against a `main` build), and a malformed proxy URL now fails with `Invalid HTTPS_PROXY value "...": `, naming the setting and redacting any credentials instead of surfacing a bare `Invalid URL` (4 new tests, 614 total) | diff --git a/SPECS/deployment.md b/SPECS/deployment.md index 2488ad1..3527044 100644 --- a/SPECS/deployment.md +++ b/SPECS/deployment.md @@ -16,10 +16,11 @@ Triggers on: push and pull requests to `main`. -Matrix: Node.js 18, 20, 22. +Matrix: Node.js 20, 22. Jobs: -- **build-and-test**: checkout → setup node → install → generate API client → type check → build → test +- **build-and-test**: checkout → setup node → install → generate API client → type check → build → smoke test the built CLI → test + - The smoke step runs `node dist/index.js --version` three ways (plain, with `HTTPS_PROXY` set, and with an unreadable `SSL_CERT_FILE` expected to fail). It is the only thing that executes the real entry point — every command test builds a bare `new Command()` — so it is what catches a dependency that loads or constructs fine on one Node version but not another. - **changeset-check** (PRs only): verifies at least one `.changeset/*.md` file is present in the PR diff ### Release (`release.yml`) diff --git a/package-lock.json b/package-lock.json index 428bee8..3ed3250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@codacy/codacy-cloud-cli", - "version": "1.6.0", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@codacy/codacy-cloud-cli", - "version": "1.6.0", + "version": "1.9.0", "license": "ISC", "dependencies": { - "@codacy/tooling": "0.1.0", + "@codacy/tooling": "0.23.0", "ansis": "4.0.0", "cli-table3": "^0.6.3", "commander": "14.0.0", @@ -574,10 +574,13 @@ } }, "node_modules/@codacy/tooling": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@codacy/tooling/-/tooling-0.1.0.tgz", - "integrity": "sha512-Q6Dx1AR39tDT6Oc2PmZ9b0RfzbqmYB4/+TMbT1JaNB7ZTAUhjaLSVD/JtwkNHDoWPKaKdqfaMbBmjXoram3FJg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@codacy/tooling/-/tooling-0.23.0.tgz", + "integrity": "sha512-o7awf90snA+i6BBx0wBW+K561U9X0qp38/gXh+32jxsxWVQkaIn4RcsVjrGYBA8Ei9RHc+F3Yt0cubofPB6gVQ==", "license": "MIT", + "dependencies": { + "undici": "^6.21.0" + }, "engines": { "node": ">=20.0.0" } @@ -4315,6 +4318,15 @@ "node": ">=0.8.0" } }, + "node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/package.json b/package.json index 6b46ec3..720df9a 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "node": ">=20" }, "dependencies": { - "@codacy/tooling": "0.1.0", + "@codacy/tooling": "0.23.0", "ansis": "4.0.0", "cli-table3": "^0.6.3", "commander": "14.0.0", diff --git a/src/index.ts b/src/index.ts index d6cbd6e..976bc64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { cliVersion } from "./version"; import { getOutputFormat } from "./utils/output"; import { BASE_HEADERS, repositoryTokenOption } from "./utils/auth"; import { maybeNotifyUpdate } from "./utils/update-check"; +import { configureProxyFromEnv } from "./utils/proxy"; import { registerInfoCommand } from "./commands/info"; import { registerRepositoriesCommand } from "./commands/repositories"; import { registerRepositoryCommand } from "./commands/repository"; @@ -25,6 +26,14 @@ import { registerLogoutCommand } from "./commands/logout"; const program = new Command(); +// Route all outbound fetch traffic through HTTP(S)_PROXY / NO_PROXY and any +// corporate CA before anything can make a request. Delegated to +// `@codacy/tooling` so the environment contract is identical to the Codacy +// Analysis CLI. No-op when no proxy or TLS variable is set. It only has to run +// before `program.parse` — every request happens inside a command action — but +// it goes first so the network stack is configured before we point it at the API. +configureProxyFromEnv(); + OpenAPI.BASE = (process.env.CODACY_API_BASE_URL || "https://app.codacy.com").replace(/\/$/, "") + "/api/v3"; // No token here. Which header carries it depends on the token kind, which isn't // known until a command resolves its auth — every API path installs headers diff --git a/src/utils/proxy.test.ts b/src/utils/proxy.test.ts new file mode 100644 index 0000000..964be0d --- /dev/null +++ b/src/utils/proxy.test.ts @@ -0,0 +1,82 @@ +/** + * Unit tests for the proxy/TLS startup hook (`configureProxyFromEnv`). + * + * The proxy behavior itself lives in `@codacy/tooling` and is covered by its own + * suite — env-var precedence, `NO_PROXY` matching, bare `host:port` + * normalization, PEM validation. Re-testing any of that here would freeze + * upstream's internals against us, so `configureProxy` is mocked and these tests + * pin only the seam we own: that we delegate with no overrides (env is the sole + * input, which is what keeps the contract identical to the Analysis CLI), and + * that a misconfigured CA bundle is fatal with the message preserved verbatim. + */ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +// Hoisted so the spy exists before the mock factory and the module under test load. +const { configureProxySpy } = vi.hoisted(() => ({ configureProxySpy: vi.fn() })); + +// Only `configureProxy` is needed: `src/types/codacy-config.ts` also imports from +// this module, but via `export type`, which is erased at compile time. +vi.mock("@codacy/tooling", () => ({ configureProxy: configureProxySpy })); + +import { configureProxyFromEnv } from "./proxy"; + +describe("configureProxyFromEnv", () => { + let errorSpy: ReturnType; + + beforeEach(() => { + // handleError is exercised for real, so process.exit has to be stubbed to + // throw — otherwise it would tear down the test run. + vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit called"); + }); + errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + configureProxySpy.mockReset(); + }); + + it("delegates proxy setup to @codacy/tooling, with no overrides", () => { + configureProxyFromEnv(); + + expect(configureProxySpy).toHaveBeenCalledOnce(); + // The zero-argument call is the contract: passing overrides would make this + // CLI's proxy behavior diverge from every other Codacy tool. + expect(configureProxySpy.mock.calls[0]).toEqual([]); + }); + + it("does nothing and does not throw when no proxy is configured", () => { + // Mirrors tooling's early return when no proxy/TLS variable is set. + configureProxySpy.mockImplementation(() => {}); + + expect(() => configureProxyFromEnv()).not.toThrow(); + expect(errorSpy).not.toHaveBeenCalled(); + }); + + it("exits with the tooling error message when the CA bundle is unreadable", () => { + configureProxySpy.mockImplementation(() => { + throw new Error( + "Failed to read CA certificate from /nope.pem: ENOENT: no such file or directory", + ); + }); + + expect(() => configureProxyFromEnv()).toThrow("process.exit called"); + // Asserted as a substring so the message survives ansis colorization. The + // point is that we relay tooling's text unchanged rather than rewording it. + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("Failed to read CA certificate from /nope.pem"), + ); + }); + + it("exits on a non-Error throw", () => { + configureProxySpy.mockImplementation(() => { + throw "not an Error"; + }); + + expect(() => configureProxyFromEnv()).toThrow("process.exit called"); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("An unknown error occurred."), + ); + }); +}); diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts new file mode 100644 index 0000000..0110de7 --- /dev/null +++ b/src/utils/proxy.ts @@ -0,0 +1,65 @@ +/** + * Proxy and TLS configuration for outbound requests. + * + * Every network call in this CLI goes through Node's native global `fetch` — the + * generated API client in `src/api/client/core` and the MITRE CVE lookup in + * `commands/finding.ts` — and Node's `fetch` does **not** honor + * `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` the way `curl` does. Behind a corporate + * proxy, requests simply hang or fail. + * + * The implementation is deliberately **not** ours: `configureProxy()` from + * `@codacy/tooling` installs a global `undici` dispatcher that routes each + * request by protocol, honors `NO_PROXY` per request, normalizes a bare + * `host:port` proxy value, and loads a corporate CA bundle. Delegating to it + * keeps the environment contract identical to the Codacy Analysis CLI and the + * VSCode extension, so one corporate-proxy setup drives all of them: + * + * HTTPS_PROXY / HTTP_PROXY (or lowercase) — proxy URL per scheme + * NO_PROXY / no_proxy — hosts that bypass the proxy + * SSL_CERT_FILE / NODE_EXTRA_CA_CERTS — PEM CA bundle to trust + * CODACY_CLI_INSECURE — disable TLS verification (warns) + * + * A no-op when none of those are set — and a free one. `@codacy/tooling` loads + * `undici` lazily, only once `configureProxy` gets past its "nothing + * configured" early-out, so an unproxied run pays nothing: measured at 0 ms + * median against a `main` build (20 interleaved `--version` runs, Node 20). + * That property is upstream's to keep, and it is guarded there by + * `packages/tooling/test/proxy-lazy-undici.test.ts`. It is worth re-measuring + * on a major bump, since a top-level `undici` import upstream would silently + * put ~27 ms back onto every invocation, `--help` and `--version` included — + * which is what 0.22.0 did before this was fixed. + * + * Note the "update available" notice is unaffected: `update-notifier` uses its + * own `got` stack, which honors neither this dispatcher nor the proxy variables. + * Behind a strict proxy, disable it with `CODACY_DISABLE_UPDATE_CHECK=1`. + */ +import { configureProxy } from "@codacy/tooling"; + +import { handleError } from "./error"; + +/** + * Apply proxy/TLS settings from the environment. Call once at startup, before + * any command can make a request. + * + * A misconfigured setting is **fatal by design**, and the catch is deliberately + * broad rather than tied to a specific failure. `configureProxy` throws on an + * unreadable or non-PEM CA bundle and on a malformed proxy URL, and the exact + * set has already changed once across a minor bump. Enumerating it here would + * just rot: this wrapper's contract is "any failure to apply the requested + * configuration is fatal", and upstream owns which failures exist and how they + * read. Upstream also redacts credentials before echoing a bad proxy value, so + * passing the message straight through does not leak a proxy password. + * + * Unlike `maybeNotifyUpdate`, which swallows everything because an update check + * must never break the CLI, swallowing here would leave the user running with + * configuration they believe is in effect — silently falling back to the system + * trust store or to a direct connection — and hand them a confusing TLS or + * timeout error later instead of the real cause now. + */ +export function configureProxyFromEnv(): void { + try { + configureProxy(); + } catch (err) { + handleError(err); + } +}