Skip to content

feat!: resolve SDK auth via OS keychain through createSDK helper - #3999

Merged
eablack merged 8 commits into
v12.0.0from
eb/cli-createsdk-token-resolution
Oct 19, 2026
Merged

eablack merged 8 commits into
v12.0.0from
eb/cli-createsdk-token-resolution

Conversation

@eablack

@eablack eablack commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Centralizes all HerokuSDK / HerokuApiClient construction in the CLI behind a single createSDK / createApiClient helper (src/lib/sdk.ts), so the CLI-resolved auth token and platform baseUrl are threaded in one place instead of being re-derived — or omitted — at ~170 call sites. An ESLint no-restricted-syntax guard bans new HerokuSDK / new HerokuApiClient anywhere outside src/lib/sdk.ts to keep construction centralized going forward.

Each call site still specifies its own extensions (and any other HerokuSDKOptions): the helper is generic over the extension tuple (createSDK<const Exts>(…): HerokuSDK<Exts>), so the extension-augmented resource types are preserved on the returned SDK. 63 of the 171 call sites pass extensions, across 17 distinct extension sets.

⚠️ Behavior change: SDK-backed commands now use the OS keychain, not ~/.netrc

This is the headline behavioral change and the reason for the feat! / major bump.

  • Before: ~169 of 172 SDK constructions passed no token (97 bare new HerokuSDK() plus ~70 passing only {extensions}). On heroku-fetch 0.1.x a tokenless client falls through to heroku-fetch's default provider, which resolves HEROKU_API_KEY → ~/.netrc (via netrc-parser). The OS keychain was never consulted on that path. (A minority of sites already threaded heroku.auth, which is keychain-sourced.)
  • After: every construction goes through createSDK / createApiClient, which resolve the token via APIClient.getAuthEntry() → @heroku/heroku-credential-manager → OS keychain (the same store the rest of the CLI uses), with HEROKU_API_KEY still taking precedence as an override.

Net effect: SDK-backed commands now read credentials from the same OS keychain as the rest of the CLI. Users authenticated via heroku login (which writes the keychain) are unaffected or fixed; any workflow that relied on a hand-maintained ~/.netrc entry for these commands must ensure the credential is in the keychain.

The SDK and heroku-fetch take on no credential-manager dependency — the Node-only keychain integration lives entirely in the CLI's createSDK helper via the existing token / TokenProvider seam, so heroku-fetch stays Node-agnostic / browser-safe.

Follow-up (not in this PR)

Bumping the CLI's direct @heroku/heroku-fetch to ^0.2.0 (which drops the implicit ~/.netrc fallback entirely) is deferred to a separate atomic flip, to be done once @heroku/sdk@0.11.0 — which bundles heroku-fetch 0.2.0 — ships, so the direct dep and the SDK's transitive dep move together rather than producing a dual-version tree. This PR is behavior-neutral with respect to that bump: createSDK threads the token explicitly, so it is forward-compatible with both 0.1.x and 0.2.0.

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)

  • refactor: Refactoring existing code without changing behavior
  • test: Add/update/remove tests

Testing

Notes:
createSDK resolves the token lazily (a TokenProvider function, not a snapshot), so a heroku login / logout after construction is honored. Unit tests mock APIClient.getAuthEntry at the resolution seam.

Steps:

  1. Passing CI suffices (full unit suite: 2480 passing, 0 failing).
  2. Manually verified against the live API with a keychain credential: heroku status, heroku apps, heroku logs -n, and heroku apps:errors all succeed.
  3. Confirmed heroku login writes the OS keychain (service heroku-cli, account updated at login time) and that nothing is written to ~/.netrc (empty, zero heroku entries, untouched).

Related Issues

GUS work item: W-23096184

@eablack
eablack requested a review from a team as a code owner October 16, 2026 20:52
@github-actions github-actions Bot added size/XL Extra large PR (code churn >= 1000); consider splitting docs Touches documentation labels Oct 16, 2026
@github-actions

Copy link
Copy Markdown

This PR is labeled size/XL (29384 lines of code churn, excluding docs, package-lock.json, and generated files).

Large PRs are harder to review well. If it can be split into smaller, independently reviewable pieces, consider doing so. This is a suggestion, not a blocker — the check still passes.

@eablack
eablack changed the base branch from main to v12.0.0 October 16, 2026 20:55
Route all HerokuSDK / HerokuApiClient construction through a single
createSDK / createApiClient helper in src/lib/sdk.ts so the CLI-resolved
token and platform baseUrl are threaded in one place rather than
re-derived (or omitted) at ~170 call sites. Each call site may still pass
its own `extensions` (and any other HerokuSDKOptions); the helper's
generic preserves the extension-augmented resource types on the returned
SDK. An ESLint no-restricted-syntax guard bans `new HerokuSDK` /
`new HerokuApiClient` outside src/lib/sdk.ts to keep construction
centralized.

The helper threads a lazy TokenProvider backed by APIClient.getAuthEntry(),
so SDK calls resolve the token on the same path as the rest of the CLI
(OS keychain via credential-manager, HEROKU_API_KEY override preserved)
while the SDK and heroku-fetch take on no credential-manager dependency
(stay Node-agnostic / browser-safe).

BREAKING CHANGE: SDK-backed commands now resolve their auth token from the
OS keychain (via APIClient.getAuthEntry) instead of ~/.netrc. Previously
most SDK-backed commands constructed the SDK without a token and fell
through to heroku-fetch 0.1.x's default provider, which read
HEROKU_API_KEY then ~/.netrc. Users authenticated via `heroku login`
(which writes the OS keychain) are unaffected; any workflow relying on a
hand-maintained ~/.netrc entry for these commands must ensure the
credential is in the keychain.
@eablack
eablack force-pushed the eb/cli-createsdk-token-resolution branch from 81d637d to 1ac4d67 Compare October 16, 2026 21:03
@eablack
eablack deployed to AcceptanceTests October 16, 2026 21:03 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 16, 2026 21:03 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 16, 2026 21:03 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 16, 2026 21:03 — with GitHub Actions Active
@github-actions github-actions Bot removed the docs Touches documentation label Oct 16, 2026

@tlowrimore-heroku tlowrimore-heroku 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.

One real gap (inline on ps/index.ts): some @heroku/sdk 0.10.0 resources build their own HerokuApiClient from the per-call options.clientOptions, not from the SDK-level options. createSDK's token and baseUrl never reach those calls, so ps --extended and run:inside still go through heroku-fetch's netrc / HEROKU_API_KEY default.

The other comments are smaller cleanups.

Comment thread src/commands/ps/index.ts Outdated
Comment thread src/lib/run/dyno.ts
Comment thread src/commands/status.ts Outdated
Comment thread src/commands/ps/autoscale/disable.ts Outdated
Comment thread eslint.config.js
Comment thread test/unit/commands/apps/destroy.unit.test.ts
… guard

Order the message/selector keys alphabetically in the no-restricted-syntax
entries so `eslint .` passes; no behavior change to the SDK/client guard.
@eablack
eablack deployed to AcceptanceTests October 16, 2026 22:59 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 16, 2026 22:59 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 16, 2026 22:59 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 16, 2026 22:59 — with GitHub Actions Active
listExtended and run:inside's exec path (postRun) each build a bare
HerokuApiClient from per-call clientOptions rather than the SDK-level
client from createSDK, so the CLI-resolved token never reached them.
Pass clientOptions: apiClientOptions(heroku) at both call sites and add
regression tests that assert the outgoing Authorization header via nock
with a stubbed getAuthEntry sentinel.
status needs a tokenless platform client for the public trust endpoint;
replace its ad-hoc APIClient construction with a single
createUnauthenticatedClient(baseUrl) helper in sdk.ts so unauthenticated
client construction lives in one place alongside createSDK/createApiClient.
disable/enable each called createSDK twice to pull metrics and platform
separately; destructure both from one createSDK(this.heroku) call.
Match callee.property.name so member-expression constructions of HerokuSDK
and HerokuApiClient are also flagged, and note that a later flat-config
block setting the same rule replaces rather than merges its options.
The comments still described the old clientOptions: {token: this.heroku.auth}
wiring; describe createSDK's token provider (tokenFor -> getAuthEntry) instead.
@eablack
eablack deployed to AcceptanceTests October 16, 2026 23:13 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 16, 2026 23:13 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 16, 2026 23:13 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 16, 2026 23:13 — with GitHub Actions Active

@tlowrimore-heroku tlowrimore-heroku 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!

sdk 0.11.0 depends on @heroku/heroku-fetch ^0.2.0; bump the CLI's own
heroku-fetch pin from ^0.1.6 to match so both resolve to a single copy.
The skew otherwise left two structurally-incompatible HerokuApiClientOptions
types colliding in src/lib/sdk.ts. Also drop the version-pinned heroku-fetch
reference from the tokenFor comment so it can't go stale.
@eablack
eablack deployed to AcceptanceTests October 19, 2026 16:57 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 19, 2026 16:57 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 19, 2026 16:57 — with GitHub Actions Active
@eablack
eablack deployed to AcceptanceTests October 19, 2026 16:57 — with GitHub Actions Active
@eablack
eablack merged commit 505d4b1 into v12.0.0 Oct 19, 2026
20 checks passed
@eablack
eablack deleted the eb/cli-createsdk-token-resolution branch October 19, 2026 17:18

This branch was successfully deployed

1 active deployment
AcceptanceTests — 0c0f14e2 Deployed Oct 19, 2026 by eablack via acceptance (24.x, ubuntu-latest) #9489
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large PR (code churn >= 1000); consider splitting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants