fix(cli): mint a JWT for --host remote targets when using an API key

Closed
AN
2mo ago
+279-3

Fixes #6315

Problem

--api-key (and SUPERSET_API_KEY) is documented as a global option, but it is ignored on any command that targets a remote host via --host <machineId>. Those commands exit 1 with:

Error: Session expired
Hint: Run: superset auth login

The message is doubly misleading: the API key is not expired (the same key succeeds against the local host and against hosts list in the same shell), and no session was ever established.

Root cause

The relay authenticates host-service traffic only by verifying a JWKS-signed JWT. The --host <remote> transport (resolveHostTarget.ts) sent the raw bearer — an sk_live_… API key from --api-key / SUPERSET_API_KEY — straight into the Authorization: Bearer header. An API key is not a JWT, so the relay returned UNAUTHORIZED, and the CLI's generic handler rendered any UNAUTHORIZED as "Session expired".

Every other relay client already performs an API-key → JWT exchange (JwtAuthProvider.getJwt, packages/sdk/src/client.ts). The CLI's remote-host path was the missing one.

Fix

  • New packages/cli/src/lib/host-jwt.ts — getHostJwt(bearer), mirroring the exchange JwtAuthProvider and the SDK already perform:
    • JWT-shaped tokens (CLI OAuth access tokens are JWKS-signed) pass straight through, no exchange needed;
    • sk_live_ / sk_test_ API keys are exchanged for a JWT via GET {api}/api/auth/token with the key in the x-api-key header (better-auth's apiKey plugin reads that header, not Authorization), cached in-process ~55 min (5 min refresh buffer), keyed by credential.
  • resolveHostTarget.ts — the remote branch now mints the JWT inside an async headers() hook (tRPC's httpBatchLink supports an async headers function). This confines the change to the remote branch; all ~16 call sites (workspaces/*, projects/*, terminals/*, agents/*, automations/*, …) are untouched.

No relay or host-service change is needed — the minted JWT carries the same organizationIds/sub claims the relay's checkHostAccess already verifies.

Tests

packages/cli/src/lib/host-jwt.test.ts (6 cases, bun:test, mock fetch + getApiUrl):

  • OAuth JWT passes through with no exchange;
  • sk_live_ / sk_test_ keys are exchanged via /api/auth/token with x-api-key;
  • minted JWT is cached per credential and reused (single fetch);
  • different credentials do not share a cached JWT;
  • exchange failure throws.

Manual verification

The repro pair from the issue (superset workspaces list --host "$REMOTE" --api-key "$KEY") now authenticates; hosts list already worked via the x-api-key path and is unchanged.


Summary by cubic

Fixes remote --host CLI commands using --api-key by minting a JWT before calling the relay, so API key auth works instead of failing with "Session expired" (fixes #6315).

  • Bug Fixes
    • Added packages/cli/src/lib/host-jwt.ts: getHostJwt exchanges sk_live_/sk_test_ keys for a JWT via GET {api}/api/auth/token with x-api-key, caches 55 min per credential, validates a trimmed non-empty string before caching, passes JWT-like tokens through, and uses a 10s AbortSignal.timeout.
    • Updated resolveHostTarget.ts to mint the JWT in an async headers() hook for httpBatchLink, leaving call sites unchanged.
    • Expanded host-jwt.test.ts to cover invalid/missing/whitespace-only tokens (not cached), per-key cache isolation, a real timeout signal, and the 55‑min cache window boundary (re-mints just past the window).

Summary by CodeRabbit

  • New Features

    • Remote host connections now support API-key authentication through automatic token exchange.
    • Existing JWT credentials continue to pass through directly.
    • Authentication tokens are securely reused for a limited time to reduce repeated requests.
  • Bug Fixes

    • Improved authentication handling for remote host relay requests.
    • Authentication failures now report relevant HTTP status information.
    • Invalid authentication responses are rejected.
    • Token exchanges now time out to prevent stalled requests.