Repository navigation
refactor!: route credential handling through heroku-credential-manager and take login off http-call - #448
Merged
Conversation
…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.
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
left a comment
Contributor
There was a problem hiding this comment.
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.
michaelmalave
approved these changes
Oct 19, 2026
michaelmalave
left a comment
Contributor
There was a problem hiding this comment.
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
added a commit
that referenced
this pull request
Oct 20, 2026
…r and take login off http-call (#448)
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Thins
@heroku-cli/commandfor the v14 major by handing shared auth to the published@heroku/heroku-credential-managerpackage and taking the login transport off@heroku/http-call. Four commits:refactor!— Route credential handling through@heroku/heroku-credential-manager.credential-manager-coreanddeprecatedsnapshots; the adapter/handlers/netrc-parser/login-state/storage-selector logic now lives in the published package (behavior parity verified byte-for-byte).src/env-auth.ts(resolveEnvToken) as the Node adapter around the package's environment-agnosticresolveEnvAuth.src/credential-error-reporter.ts, wired once viasetCredentialErrorReporter.@deprecatedcredential re-exports from the package barrel.chore— Drop dependencies and a test helper orphaned by the removal: runtime depsexeca(git usesnode:child_process),@heroku/types,tsheredoc; dev depsproxyquire/@types/proxyquire,fs-extra/@types/fs-extra,@types/supports-color; andtest/helpers/unwrap.ts.refactor— Run the login fetch transport onfetchinstead of http-call. The credential-manager login module takes aFetchLike, owns its own poll retry, and setsredirect: 'error'+ itsAbortSignalon every request, so the http-call-backed adapter is replaced with afetch-based adapter routed through heroku-fetch'sgetDefaultDispatcher()forHTTP(S)_PROXY/NO_PROXY. When a proxy dispatcher is present the request runs on the installed undici's ownfetch; with no proxy configured it uses Node's built-infetchdirectly. Custom CAs are honored via the standardNODE_EXTRA_CA_CERTS. Adds@heroku/heroku-fetchas a direct dependency.fix— Address review findings, all specific to this repo:getDefaultDispatcher()builds anEnvHttpProxyAgentfrom the installed undici, which Node's built-infetch(a different bundled undici copy) cannot drive — it throwsinvalid onRequestStart method. Proxied requests now run on the installed undici's ownfetch, decomposing any global-constructorRequestinto(url, init)since the brand check is per-copy. Adds a directundicidependency pinned to the range heroku-fetch uses so npm dedupes to a single copy. (Temporary bridge: once heroku-fetch exports a matchedfetch+dispatcher pair, the directundicidep and the decomposition helper can be dropped.)timeout: undefined, whichObject.assignoverwrote http-call's default 60s timeout with, letting a request hang indefinitely. Signal/timeout are now set only when the caller supplies them.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 publishedCredentialErrorContexttype.Net: ~7,400 lines removed.
api-client.tsandparticleboard-client.ts(and the http-call types they still expose throughHerokuAPIErrorand the login Platform adapter) are intentionally left for a later PR.Type of Change
Breaking Changes (major semver update)
!after your change type to denote a change that breaks current behaviorFeature Additions (minor semver update)
Patch Updates (patch semver update)
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_DIRto the standardNODE_EXTRA_CA_CERTS. The proxy fix is covered by real-proxy regression tests (a proxied GET and a body-bearingRequestdriven through the installed undici).Steps:
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)