From 1728845909d2c4e4aa979f9966ad9dd31f5994eb Mon Sep 17 00:00:00 2001 From: Alejandro Rizzo Date: Mon, 7 Sep 2026 16:23:46 +0100 Subject: [PATCH 1/3] feat: add HTTP/HTTPS proxy and TLS support (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node's global fetch — used by the generated API 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, delegate to configureProxy() from @codacy/tooling (bumped 0.1.0 -> ^0.22.0), the same function the Codacy Analysis CLI calls. It installs a global undici dispatcher with per-request protocol and NO_PROXY routing, bare host:port normalization, and SSL_CERT_FILE/NODE_EXTRA_CA_CERTS CA loading. Keeping the implementation upstream is what keeps the environment contract identical across the Codacy tools; a local copy would drift. src/utils/proxy.ts is a thin seam: configureProxyFromEnv() calls it and routes its deliberate fail-loud throw (unreadable or 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; this CLI does not, and exits 1 everywhere. Called at the top of src/index.ts. Ordering is only constrained to precede program.parse, since the dispatcher is resolved per request — it goes first so the network stack is set up before we point it at the API. Kept top-level rather than in the preAction hook so a typo'd SSL_CERT_FILE fails even on --version. Also add a CI smoke step that runs the built entry point three ways (plain, with HTTPS_PROXY set, and with a bad SSL_CERT_FILE expected to fail). Nothing previously executed src/index.ts — every command test builds a bare new Command() — which is how a proxy dependency that cannot even load on Node 20 could pass CI. Upstream owns the proxy semantics and their 24 tests, so only the seam is tested here (4 new tests, 614 total). Supersedes #39. Co-Authored-By: rattalur <145406381+rattalur@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/proxy-tls-support.md | 18 ++++++++ .github/workflows/ci.yml | 24 ++++++++++ AGENTS.md | 6 +++ README.md | 27 ++++++++++- SPECS/README.md | 1 + SPECS/deployment.md | 5 +- package-lock.json | 24 +++++++--- package.json | 2 +- src/index.ts | 9 ++++ src/utils/proxy.test.ts | 82 +++++++++++++++++++++++++++++++++ src/utils/proxy.ts | 48 +++++++++++++++++++ 11 files changed, 236 insertions(+), 10 deletions(-) create mode 100644 .changeset/proxy-tls-support.md create mode 100644 src/utils/proxy.test.ts create mode 100644 src/utils/proxy.ts diff --git a/.changeset/proxy-tls-support.md b/.changeset/proxy-tls-support.md new file mode 100644 index 0000000..19b8234 --- /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. An unreadable or non-PEM CA bundle fails immediately with a clear error instead of silently falling back to the default trust store. + +Nothing changes when no proxy variable is set. + +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..6c8b653 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` (bumped `0.1.0` → `^0.22.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 (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..ba22fe4 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.22.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.22.0", + "resolved": "https://registry.npmjs.org/@codacy/tooling/-/tooling-0.22.0.tgz", + "integrity": "sha512-Ee+MwBRZ9WmvCzDsyjChu17v2ZKerMwQPNixLTxnb4I9ELipNAFM1Kw0UPXcLbX2vA/MIOJiAu84Cn2FnDUHQw==", "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..3b8ac7d 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "node": ">=20" }, "dependencies": { - "@codacy/tooling": "0.1.0", + "@codacy/tooling": "^0.22.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..2b6cdb4 --- /dev/null +++ b/src/utils/proxy.ts @@ -0,0 +1,48 @@ +/** + * 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) + * + * No-op when none of those are set, so the default path is unchanged. + * + * 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 CA bundle is **fatal by design** — it is the one thing + * `configureProxy` throws on. Unlike `maybeNotifyUpdate`, which swallows + * everything because an update check must never break the CLI, swallowing here + * would silently fall back to the system trust store and hand the user a + * confusing TLS error later instead of the real cause now. + */ +export function configureProxyFromEnv(): void { + try { + configureProxy(); + } catch (err) { + handleError(err); + } +} From 06a1f2e03af207a64642130265b640656aee542a Mon Sep 17 00:00:00 2001 From: Alejandro Rizzo Date: Tue, 8 Sep 2026 11:15:07 +0100 Subject: [PATCH 2/3] fix: correct proxy doc claims and pin @codacy/tooling exactly Follow-up to an adversarial review of this branch. Three claims were wrong and one dependency range did not do what it was chosen to do. - Pin @codacy/tooling to exact 0.22.0, was ^0.22.0. The caret was chosen so upstream proxy fixes would arrive without a bump PR, but for a pre-1.0 package the caret spans patches only (^0.22.0 resolves to >=0.22.0 <0.23.0-0), so it never would have picked up a 0.23.0. It bought silent patch drift with no proxy coverage in this repo to catch a regression, and none of the upside. Bumps are now deliberate. Also drops the claim that every other dependency here is pinned exactly -- cli-table3 was already ^0.6.3 before this branch. - configureProxyFromEnv's doc claimed a bad CA bundle is "the one thing configureProxy throws on". It is not: a malformed proxy URL throws too, as whatever new URL() or undici's ProxyAgent raises. Verified against the installed 0.22.0 -- HTTPS_PROXY="not a url" gives `Error: Invalid URL`, ftp:// gives `Error: invalid url`. The catch is intentionally broad, so the comment now states that contract instead of enumerating a list that rots. - "No-op when nothing is set" was true of behavior but not of cost. 0.22.0 imports undici at module scope rather than behind configureProxy's early-out, so every invocation pays it, --help and --version included. Measured ~27 ms median against a ~119 ms baseline (20 interleaved runs, Node 20). Disclosed in the module header and the changeset rather than left implied. Upstream 0.23.0 (published today) moves that import behind a lazy factory and adds proxy-URL validation with credential redaction. Not taken here: it cannot be installed or verified in this environment. Its CA-bundle error text is unchanged, so the CI smoke step's substring assertion survives the bump. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/proxy-tls-support.md | 2 +- SPECS/README.md | 2 +- package-lock.json | 2 +- package.json | 2 +- src/utils/proxy.ts | 26 ++++++++++++++++++++------ 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.changeset/proxy-tls-support.md b/.changeset/proxy-tls-support.md index 19b8234..6ebf315 100644 --- a/.changeset/proxy-tls-support.md +++ b/.changeset/proxy-tls-support.md @@ -13,6 +13,6 @@ Every command now honors the standard environment variables: 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. An unreadable or non-PEM CA bundle fails immediately with a clear error instead of silently falling back to the default trust store. -Nothing changes when no proxy variable is set. +Behavior is unchanged when no proxy variable is set. Note that startup does get slightly slower either way — around 27ms — because the proxy dependency is loaded eagerly; a future dependency bump will reclaim that. Thanks to @rattalur for reporting the gap and for the initial implementation in #39. diff --git a/SPECS/README.md b/SPECS/README.md index 6c8b653..8beb84e 100644 --- a/SPECS/README.md +++ b/SPECS/README.md @@ -86,4 +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` (bumped `0.1.0` → `^0.22.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 (4 new tests, 614 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.22.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 (4 new tests, 614 total) | diff --git a/package-lock.json b/package-lock.json index ba22fe4..cb8ca80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.9.0", "license": "ISC", "dependencies": { - "@codacy/tooling": "^0.22.0", + "@codacy/tooling": "0.22.0", "ansis": "4.0.0", "cli-table3": "^0.6.3", "commander": "14.0.0", diff --git a/package.json b/package.json index 3b8ac7d..c47d1df 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "node": ">=20" }, "dependencies": { - "@codacy/tooling": "^0.22.0", + "@codacy/tooling": "0.22.0", "ansis": "4.0.0", "cli-table3": "^0.6.3", "commander": "14.0.0", diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 2b6cdb4..f0c1257 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -19,7 +19,13 @@ * SSL_CERT_FILE / NODE_EXTRA_CA_CERTS — PEM CA bundle to trust * CODACY_CLI_INSECURE — disable TLS verification (warns) * - * No-op when none of those are set, so the default path is unchanged. + * Behaviorally a no-op when none of those are set, so an unproxied run is + * unaffected. It is not free, though: `@codacy/tooling@0.22.0` imports `undici` + * at module scope rather than behind `configureProxy`'s early-out, so the cost + * lands on every invocation, `--help` and `--version` included. Measured at + * ~27 ms median against a ~119 ms baseline (20 interleaved runs, Node 20). + * Upstream 0.23.0 moves that import behind a lazy factory; bumping to it is + * tracked in `analysis-cli`'s `docs/tech-debt.md` and should reclaim it. * * Note the "update available" notice is unaffected: `update-notifier` uses its * own `got` stack, which honors neither this dispatcher nor the proxy variables. @@ -33,11 +39,19 @@ import { handleError } from "./error"; * Apply proxy/TLS settings from the environment. Call once at startup, before * any command can make a request. * - * A misconfigured CA bundle is **fatal by design** — it is the one thing - * `configureProxy` throws on. Unlike `maybeNotifyUpdate`, which swallows - * everything because an update check must never break the CLI, swallowing here - * would silently fall back to the system trust store and hand the user a - * confusing TLS error later instead of the real cause now. + * 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 also on a malformed proxy URL — the + * latter surfacing as whatever `new URL()` or undici's `ProxyAgent` raises, + * which varies by version. Enumerating those here would just rot: this wrapper's + * contract is "any failure to apply the requested configuration is fatal", and + * upstream owns which failures exist. + * + * 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 { From 99aa25274dec8e08df9b9665f686990d7ca79e78 Mon Sep 17 00:00:00 2001 From: Alejandro Rizzo Date: Tue, 8 Sep 2026 11:53:23 +0100 Subject: [PATCH 3/3] feat: take @codacy/tooling 0.23.0, reclaiming the startup cost 0.23.0 ships the two fixes filed from this branch's review (recorded in analysis-cli's docs/tech-debt.md), and both land here: - undici now loads lazily, behind configureProxy's "nothing configured" early-out. An unproxied run pays nothing: `--version` measures 0 ms delta against a `main` build, down from 27 ms on 0.22.0 (20 interleaved runs, Node 20, median). Verified the property directly too -- requiring the tooling barrel and calling configureProxy() with no proxy env leaves undici out of require.cache, while a configured proxy still loads it. - a malformed proxy URL now names the offending setting and redacts credentials, replacing a bare `Invalid URL`: HTTPS_PROXY="ftp://user:hunter2@proxy.corp:8080" -> Error: Invalid HTTPS_PROXY value "ftp://user:***@proxy.corp:8080": unsupported scheme "ftp:"; expected a URL such as http://proxy.corp:8080 Checked for upgrade breakage: the CA-bundle error text is unchanged, so the CI smoke step's substring assertion still holds. Bare `host:port` and credentialed proxy URLs still resolve. Installing this required scoping npm's supply-chain guard rather than weakening it. ~/.npmrc has min-release-age=3, which blocked a package published an hour earlier; it now also carries `min-release-age-exclude[]=@codacy/*` (npm's documented pattern for exactly this). Verified the guard still blocks a 1-day-old third-party version. Docs updated to match: the module header no longer claims a cost that is gone, and the changeset no longer warns about it. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/proxy-tls-support.md | 4 ++-- SPECS/README.md | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- src/utils/proxy.ts | 27 +++++++++++++++------------ 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/.changeset/proxy-tls-support.md b/.changeset/proxy-tls-support.md index 6ebf315..b403c71 100644 --- a/.changeset/proxy-tls-support.md +++ b/.changeset/proxy-tls-support.md @@ -11,8 +11,8 @@ Every command now honors the standard environment variables: - `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. An unreadable or non-PEM CA bundle fails immediately with a clear error instead of silently falling back to the default trust store. +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`. -Behavior is unchanged when no proxy variable is set. Note that startup does get slightly slower either way — around 27ms — because the proxy dependency is loaded eagerly; a future dependency bump will reclaim that. +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/SPECS/README.md b/SPECS/README.md index 8beb84e..30f6de2 100644 --- a/SPECS/README.md +++ b/SPECS/README.md @@ -86,4 +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.22.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 (4 new tests, 614 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/package-lock.json b/package-lock.json index cb8ca80..3ed3250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.9.0", "license": "ISC", "dependencies": { - "@codacy/tooling": "0.22.0", + "@codacy/tooling": "0.23.0", "ansis": "4.0.0", "cli-table3": "^0.6.3", "commander": "14.0.0", @@ -574,9 +574,9 @@ } }, "node_modules/@codacy/tooling": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@codacy/tooling/-/tooling-0.22.0.tgz", - "integrity": "sha512-Ee+MwBRZ9WmvCzDsyjChu17v2ZKerMwQPNixLTxnb4I9ELipNAFM1Kw0UPXcLbX2vA/MIOJiAu84Cn2FnDUHQw==", + "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" diff --git a/package.json b/package.json index c47d1df..720df9a 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "node": ">=20" }, "dependencies": { - "@codacy/tooling": "0.22.0", + "@codacy/tooling": "0.23.0", "ansis": "4.0.0", "cli-table3": "^0.6.3", "commander": "14.0.0", diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index f0c1257..0110de7 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -19,13 +19,15 @@ * SSL_CERT_FILE / NODE_EXTRA_CA_CERTS — PEM CA bundle to trust * CODACY_CLI_INSECURE — disable TLS verification (warns) * - * Behaviorally a no-op when none of those are set, so an unproxied run is - * unaffected. It is not free, though: `@codacy/tooling@0.22.0` imports `undici` - * at module scope rather than behind `configureProxy`'s early-out, so the cost - * lands on every invocation, `--help` and `--version` included. Measured at - * ~27 ms median against a ~119 ms baseline (20 interleaved runs, Node 20). - * Upstream 0.23.0 moves that import behind a lazy factory; bumping to it is - * tracked in `analysis-cli`'s `docs/tech-debt.md` and should reclaim it. + * 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. @@ -41,11 +43,12 @@ import { handleError } from "./error"; * * 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 also on a malformed proxy URL — the - * latter surfacing as whatever `new URL()` or undici's `ProxyAgent` raises, - * which varies by version. Enumerating those here would just rot: this wrapper's - * contract is "any failure to apply the requested configuration is fatal", and - * upstream owns which failures exist. + * 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