Skip to content

refactor!: route credential handling through heroku-credential-manager and take login off http-call - #448

Merged
eablack merged 7 commits into
v14.0.0from
eb/move-env-precedence-to-credential-manager
Oct 20, 2026
Merged

eablack merged 7 commits into
v14.0.0from
eb/move-env-precedence-to-credential-manager

Conversation

@eablack

@eablack eablack commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Thins @heroku-cli/command for the v14 major by handing shared auth to the published @heroku/heroku-credential-manager package and taking the login transport off @heroku/http-call. Four commits:

  1. refactor! — Route credential handling through @heroku/heroku-credential-manager.

    • Delete the in-repo credential-manager-core and deprecated snapshots; the adapter/handlers/netrc-parser/login-state/storage-selector logic now lives in the published package (behavior parity verified byte-for-byte).
    • Repoint the credential-manager DI seam, api-client, login, and login-state-coordinator imports at the published package.
    • Add src/env-auth.ts (resolveEnvToken) as the Node adapter around the package's environment-agnostic resolveEnvAuth.
    • Relocate the Sentry credential-error reporter to src/credential-error-reporter.ts, wired once via setCredentialErrorReporter.
    • Drop the @deprecated credential re-exports from the package barrel.
    • Remove the credential-manager compatibility/handler/acceptance test trees, the package-contract test, and the rollback/packed-consumer verify scripts + their npm scripts and CI jobs.
  2. chore — Drop dependencies and a test helper orphaned by the removal: runtime deps execa (git uses node:child_process), @heroku/types, tsheredoc; dev deps proxyquire/@types/proxyquire, fs-extra/@types/fs-extra, @types/supports-color; and test/helpers/unwrap.ts.

  3. refactor — Run the login fetch transport on fetch instead of http-call. The credential-manager login module takes a FetchLike, owns its own poll retry, and sets redirect: 'error' + its AbortSignal on every request, so the http-call-backed adapter is replaced with a fetch-based adapter routed through heroku-fetch's getDefaultDispatcher() for HTTP(S)_PROXY/NO_PROXY. When a proxy dispatcher is present the request runs on the installed undici's own fetch; with no proxy configured it uses Node's built-in fetch directly. Custom CAs are honored via the standard NODE_EXTRA_CA_CERTS. Adds @heroku/heroku-fetch as a direct dependency.

  4. fix — Address review findings, all specific to this repo:

    • Proxy dispatcher cross-undici mismatch. getDefaultDispatcher() builds an EnvHttpProxyAgent from the installed undici, which Node's built-in fetch (a different bundled undici copy) cannot drive — it throws invalid onRequestStart method. Proxied requests now run on the installed undici's own fetch, decomposing any global-constructor Request into (url, init) since the brand check is per-copy. Adds a direct undici dependency pinned to the range heroku-fetch uses so npm dedupes to a single copy. (Temporary bridge: once heroku-fetch exports a matched fetch+dispatcher pair, the direct undici dep and the decomposition helper can be dropped.)
    • Platform adapter hang. The operation-token adapter passed timeout: undefined, which Object.assign overwrote http-call's default 60s timeout with, letting a request hang indefinitely. Signal/timeout are now set only when the caller supplies them.
    • Sentry under-reporting. The credential-error reporter closed the client after flushing, leaving it registered-but-disabled so the getClient() guard mistook it for live and dropped every later credential error. It now flushes but keeps the client open. Also narrows the reporter import to the published CredentialErrorContext type.

Net: ~7,400 lines removed. api-client.ts and particleboard-client.ts (and the http-call types they still expose through HerokuAPIError and the login Platform adapter) are intentionally left for a later PR.

Type of Change

Breaking Changes (major semver update)

  • Add a ! after your change type to denote a change that breaks current behavior

Feature Additions (minor semver update)

  • feat: Introduces a new feature to the codebase

Patch Updates (patch semver update)

  • fix: Bug fix
  • deps: Dependency upgrade
  • revert: Revert a previous commit
  • chore: Change that does not affect production code
  • refactor: Refactoring existing code without changing behavior
  • test: Add/update/remove tests

Headline change is breaking (refactor!): the @deprecated credential-manager-core re-exports are removed from the package barrel. Consumers import credential APIs from @heroku/heroku-credential-manager.

Testing

Notes: Behavior parity for the credential APIs was verified byte-for-byte against the published package before deletion. The login transport swap preserves the login module's own poll retry; the two one-shot login POSTs no longer transport-retry on transient 5xx, and custom CAs move from http-call's SSL_CERT_FILE/SSL_CERT_DIR to the standard NODE_EXTRA_CA_CERTS. The proxy fix is covered by real-proxy regression tests (a proxied GET and a body-bearing Request driven through the installed undici).

Steps:

  1. Passing CI suffices.
  2. npm run build — clean; npm run lint — 0 errors; npm test — 279 passing.

Related Issues

GitHub issue: N/A
GUS work item: N/A (v14 cross-repo thinning; the heroku-fetch counterpart is tracked under W-24583381)

…l-manager

Collapse the credential-core duplication now owned by the published
@heroku/heroku-credential-manager package and move env-token precedence
into it, keeping heroku-cli-command as the Node-side adapter.

- Delete src/credential-manager-core and src/deprecated snapshots; the
  adapter/handlers/netrc-parser/login-state/storage-selector logic lives
  in the published package (behavior parity verified byte-for-byte).
- Repoint the credential-manager DI seam, api-client, login, and
  login-state-coordinator imports at the published package.
- Add src/env-auth.ts (resolveEnvToken) as the Node adapter around the
  package's environment-agnostic resolveEnvAuth.
- Relocate the Sentry credential-error reporter to
  src/credential-error-reporter.ts, wired once via setCredentialErrorReporter.
- Drop the @deprecated credential re-exports from the package barrel.
- Remove the credential-manager compatibility/handler/acceptance test
  trees, the package-contract test, and the rollback/packed-consumer
  verify scripts + their npm scripts and CI jobs.

BREAKING CHANGE: the @deprecated credential-manager-core re-exports are
removed; consumers import credential APIs from
@heroku/heroku-credential-manager.
…ore removal

- Remove runtime deps no longer imported anywhere: execa (git.ts uses
  node:child_process), @heroku/types, and tsheredoc.
- Remove dev-only deps left unused after deleting the credential-manager
  test trees: proxyquire, @types/proxyquire, fs-extra, @types/fs-extra,
  @types/supports-color.
- Delete test/helpers/unwrap.ts (no remaining importers).

Build, lint (0 errors), and the 291-test suite stay green.
…tp-call

The credential-manager login module takes a FetchLike and already owns its
own poll retry, sets redirect: 'error', and passes its AbortSignal on every
request. The http-call-backed adapter only existed to layer proxy/CA/retry
onto that transport, so replace it with native fetch:

- createCredentialManagerFetchAdapter now calls global fetch, routed through
  heroku-fetch's getDefaultDispatcher() for HTTP(S)_PROXY/NO_PROXY support.
  Native fetch honors redirect: 'error' and the abort signal directly; custom
  CAs are honored through the standard NODE_EXTRA_CA_CERTS.
- Delete the http-call transport machinery (RedirectRejectingTransport,
  commandFetch, environmentAgent/proxy policy, replayable body, retry/backoff).
- Add @heroku/heroku-fetch as a direct dependency.
- Rewrite the FetchLike adapter tests against native fetch and drop the tests
  that asserted http-call's retry/jitter/backoff/CA-file internals.

The Platform adapter (createCredentialManagerPlatformAdapter) still rides on
the APIClient and its http-call error types; that moves when api-client does.
@eablack
eablack requested a review from a team as a code owner October 19, 2026 20:51
Fixes three issues found in review of the credential-manager routing work,
all specific to heroku-cli-command:

- Proxy dispatcher cross-undici mismatch: getDefaultDispatcher() builds an
  EnvHttpProxyAgent from the installed undici, which Node's built-in fetch
  (a different bundled undici copy) cannot drive — it throws
  "invalid onRequestStart method". Route proxied requests through the
  installed undici's own fetch, decomposing any global-constructor Request
  into (url, init) since the installed undici's brand check is per-copy.
  Add a direct undici dependency pinned to the same range heroku-fetch uses
  so npm dedupes to a single copy. Covered by real-proxy regression tests.

- Platform adapter hang: platformOptions passed timeout: undefined, which
  Object.assign overwrote http-call's default 60s timeout with, letting a
  request hang indefinitely. Only set signal/timeout when the caller
  supplied them.

- Sentry under-reporting: reportCredentialStoreError closed the client after
  flushing, leaving it registered-but-disabled so the getClient() guard
  mistook it for live and dropped every later credential error. Flush but
  keep the client open.

Also narrows the credential-error-reporter import to the published
CredentialErrorContext type.

@michaelmalave michaelmalave left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work moving credential storage into the shared package and thinning this one out. Most of the files looked clean against the team patterns. Two things stood out worth a look before merge: the Sentry client now stays open after the first credential error, so later unrelated crashes in the host process would be reported to the credential project too, and two of the login adapter tests would still pass if the behavior they guard broke. There is also one heads-up on custom CA handling, which splits login from the API client for anyone who sets SSL_CERT_FILE.

Comment thread test/credential-manager-login-adapters.test.ts Outdated
Comment thread test/credential-manager-login-adapters.test.ts Outdated
Comment thread src/credential-error-reporter.ts
Comment thread src/credential-manager-login-adapters.ts

@michaelmalave michaelmalave left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Claude found a few things + those are commented but nothing blocking.

The credential error reporter keeps its Sentry client open after the first
credential failure (so a later credential error in the same process still
reports). But Sentry.init installs the default integrations — process-global
uncaught-exception / unhandled-rejection handlers plus console and http
breadcrumbs — on the global client. Left open, that routed any later unrelated
host crash to the credential DSN under release @heroku-cli/command, with
breadcrumbs that could carry console output or HTTP activity (e.g. printed
config values) the credential-shaped scrubber was not built for.

Pass defaultIntegrations: false so the client only reports the credential
errors we explicitly capture; keeping it open across captures is now harmless.
Two adapter tests passed regardless of the behavior they claimed to guard.

The redirect test asserted only rejectedWith(TypeError). Native fetch reports
every network failure as TypeError, so a regression that followed the redirect
and failed the follow-up looked identical to a refused redirect. Rebuild it on
real local servers (nock neither reproduces undici's redirect-error shape nor
follows redirects): a 302 redirector points at a 200 target, and the test now
asserts the rejection cause is redirect-specific and the target was never hit —
so following the redirect resolves and fails the test.

The bodyless-status test replied to 204/205/304 with no body, making the
empty-text assertion trivially true. Restore an on-the-wire body so the empty
result actually proves the adapter drops a body the server should not have sent.
@eablack
eablack merged commit fa1d553 into v14.0.0 Oct 20, 2026
11 checks passed
@eablack
eablack deleted the eb/move-env-precedence-to-credential-manager branch October 20, 2026 17:32
eablack added a commit that referenced this pull request Oct 20, 2026
@eablack eablack mentioned this pull request Oct 20, 2026
1 task done
eablack added a commit that referenced this pull request Oct 20, 2026
Bring the current v14.0.0 source into beta, including the #448 credential
manager refactor (login off http-call) and the main-line catch-up from the
recent v14.0.0 rebase (env-derived no-cache flag defaults, dependency bumps).

Preserve the published 14.0.0-beta.2 package version, lockfile, beta manifest,
and changelog baseline so the next Release Please candidate is 14.0.0-beta.3.
No npm release is triggered by this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants