Repository navigation
Description
Summary
--api-key is documented as a global option — "Use a Superset API key (sk_live_…) instead of OAuth login" — but it is ignored on any command that targets a remote host via --host <machineId>. Those commands fall back to the interactive OAuth session and, when none is present, exit 1 with:
Error: Session expired
Hint: Run: superset auth login
The message is misleading in two ways: the API key is not expired (the same key succeeds against the local host in the same shell, seconds apart), and no session was ever established, so nothing "expired". The real condition is this code path does not accept API-key auth.
Version: 1.20.0 · macOS 27.0.0 (arm64) · key type sk_live_…
Reproduction
Four commands, same shell, same key. A and D fail; B, C and E succeed.
$ OCI=<remote machineId> # from `superset hosts list`
# A — remote host + API key flag → FAILS
$ superset workspaces list --host "$OCI" --json --api-key "$SUPERSET_API_KEY"
Error: Session expired
Hint: Run: superset auth login
$ echo $?
1
# B — remote host + interactive OAuth session, no --api-key → WORKS
$ superset workspaces list --host "$OCI" --json | jq length
17
# C — local host + the SAME API key → WORKS (proves the key is valid, not expired)
$ superset workspaces list --json --api-key "$SUPERSET_API_KEY" | jq length
18
# D — remote host + env-var form of the same key → FAILS identically
$ SUPERSET_API_KEY=sk_live_… superset workspaces list --host "$OCI" --json
Error: Session expired
# E — `hosts list` + API key → WORKS
$ superset hosts list --json --api-key "$SUPERSET_API_KEY" | jq length
2superset projects list --host "$OCI" --json --api-key … fails the same way (exit 1, same message), so this is not specific to workspaces list.
C is the load-bearing case. It runs immediately after A with the identical key and succeeds. Whatever A is reporting, an expired credential is not it.
E is the sharp edge. An unattended agent can enumerate remote hosts with an API key, gets back real machineIds, and then cannot query a single one of them. The API surface advertises the capability and then refuses it one call later.
Expected
Either of these would resolve it — the first is much preferable:
--api-key(andSUPERSET_API_KEY) authenticate the--host <remote>path, exactly as they already do for local commands and forhosts list.- If remote host access genuinely cannot support API-key auth, then say so precisely — e.g.
Error: --host requires an interactive OAuth session; API-key auth is not supported on this path. Run: superset auth login— and note the restriction in--helpfor--hostand for the global--api-key.
Right now --help states the opposite of the behaviour: --api-key is listed under Global options with no carve-out, and --host <string> carries no auth caveat.
Why this matters
API keys exist for unattended use; OAuth login is interactive by definition. Any scheduled job that reasons across more than one machine is therefore stuck: it can run locally, it can list the hosts, and it cannot read them.
Concretely, this is a multi-host cleanup job. It must confirm that a branch is not checked out in a workspace on any host before it proposes deleting that branch. With the remote host unreadable it cannot establish that, so it correctly fails closed and withholds those targets — the job stalls on a safety check it has no unattended way to satisfy. The only workaround is a human running superset auth login before each scheduled run, which defeats the schedule.
The misleading error message compounds the cost: Session expired / Run: superset auth login reads as an ordinary credential-rotation problem, so the first response is to reissue the key — which changes nothing, because the key was never the problem.
Activity
github-actions commentedon Aug 10, 2026
Summary
The report is consistent with the code, and the mechanism is exactly as described:
--api-key/SUPERSET_API_KEYis picked up by the CLI's auth middleware, but the remote-host transport forwards that rawsk_live_…string in anAuthorization: Bearerheader to the relay, and the relay authenticates that path only by verifying a JWT against JWKS. An API key is not a JWT, so verification fails, the relay returns tRPCUNAUTHORIZED, and the CLI's generic error handler renders anyUNAUTHORIZEDasSession expired/Run: superset auth login. So both halves of the complaint hold: the--host <remote>path does not accept API-key auth, and the error text is a blanket mapping that has nothing to do with the credential actually expiring.Affected code
1. The API key is resolved correctly.
packages/cli/src/commands/middleware.ts:7callsresolveAuth(options.apiKey), andpackages/cli/src/lib/resolve-auth.ts:29-31takes theoverridebranch, settingbearerto the literalsk_live_…value. Nothing here throws — the reported "Session expired" is not coming fromresolve-auth.ts:39or:54.2. The remote transport sends the key as a bearer JWT.
packages/cli/src/lib/host-target/resolveHostTarget.ts:75-91:The option is even named
userJwt(resolveHostTarget.ts:34), but every caller passesctx.bearerunconditionally —workspaces/list/command.ts:31,projects/list/command.ts:33, plusworkspaces/{get,create,delete,open},terminals/*,agents/{list,create},projects/{create,setup},hosts/set-wake, andautomations/resolveAutomationTarget.ts:56. All of them are affected, matching the reporter's note thatprojects listfails identically.3. The relay rejects it.
apps/relay/src/index.ts:166-170→apps/relay/src/auth.ts:18-51(jwtVerifyagainst the JWKS, returnsnullon any failure) →trpcErrorResponse(c, "UNAUTHORIZED", "Unauthorized").apps/relay2/src/auth.tsis identical, so this is not a v1-vs-v2 relay difference.4. The misleading message.
packages/cli-framework/src/runner.ts:84-88maps anyUNAUTHORIZEDtRPC code to:`Error: Session expired\nHint: Run: ${cliName} auth login\n`Why B, C and E succeed
hosts list) goes throughpackages/cli/src/lib/api-client.ts:24-28, which correctly sendssk_live_…inx-api-key(better-auth's apiKey plugin reads that header, notAuthorization). API-key auth againstapi.superset.shworks.kind: "local"branch atresolveHostTarget.ts:43-72, which authenticates with the host-service manifest's localauthToken— the credential fromresolveAuthis never used on the wire. So C shows the key was accepted byresolveAuth; E is what actually proves the key is valid server-side.organizationIds, so the relay verifies it directly. That is documented inpackages/host-service/src/providers/auth/JwtAuthProvider/JwtAuthProvider.ts:56-64.Suspected root cause
The
--host <remote>path is missing the API-key → JWT exchange that every other relay client already performs. The working pattern exists twice in the repo:packages/host-service/src/providers/auth/JwtAuthProvider/JwtAuthProvider.ts:46-81— passes JWTs through, and forsk_live_…doesGET {api}/api/auth/tokenwithx-api-key, caching the result for 55 min.packages/sdk/src/client.ts:590-630— same exchange with in-memory cache and single-flight, specifically so SDK host calls can use an API key.The CLI has no equivalent; grepping
packages/cli/srcforauth/token,mintJwt, orlooksLikeJwtreturns nothing. So this looks like an omission rather than a deliberate restriction — and since the relay's owncheckHostAccess(apps/relay/src/access.ts:47-76) works off the JWT'sorganizationIdsandsub, a minted JWT should satisfy it exactly as the host-service's does.Reproduction
Manual, matching the report — the discriminating pair is:
A unit-level
bun:testreproduction that needs no relay: callresolveHostTarget({ requestedHostId: <non-local id>, organizationId, userJwt: "sk_live_test" })with a stubbedfetch/getHostId, drive one query through the returned client, and assert on the captured request headers. Today it assertsAuthorization: Bearer sk_live_test; after a fix it should carry a minted JWT (and never the rawsk_live_value inAuthorization). A second small test can pinrunner.ts'sUNAUTHORIZEDmapping so the message change doesn't regress.Suggested fix direction
Expectation 1 from the issue looks achievable without touching the relay:
JwtAuthProvider.getJwt): pass tokens that already look like JWTs straight through; forsk_live_/sk_test_prefixes,GET ${SUPERSET_API_URL}/api/auth/tokenwithx-api-keyand cache in-process.resolveHostTarget's remote branch.httpBatchLinkaccepts an asyncheaders()function, so this can stay a synchronous factory — take agetHostJwt: () => Promise<string>instead of theuserJwtstring andawaitit insideheaders(). That confines the change toresolveHostTarget.tsplus the ~12 call sites swappinguserJwt: ctx.bearerfor the getter (naturally supplied frommiddleware.tsalongsidebearer/authSource, which already knowsauthSource === "override").runner.ts:85-88shouldn't assert "expired" for everyUNAUTHORIZED. Something likeError: Not authorizedwith a hint that varies by credential source (authSourceis already onctx) avoids sending users to rotate a key that was never the problem.If for some reason API keys must stay unsupported here, then the fallback is the reporter's expectation 2: fail fast in the remote branch of
resolveHostTargetwith an explicit message, and add the carve-out to theapiKeyglobal description atpackages/cli/cli.config.ts:26-28and toapps/docs/content/docs/cli/env-vars.mdx:13, both of which currently promise unconditional equivalence to OAuth login.fix(cli): validate minted token, add fetch timeout, drop double cache…
fix(cli): reject whitespace-only minted tokens and test the fetch tim…