Repository navigation
Token resolution #1418
Description
Note
TL;DR — Three separate resolvers decided which token a command used, and they disagreed. resolveToken ignored APIFY_TOKEN, mcp install honored it, the actor entrypoint required it. On top of that, getLoggedClient() persisted whatever token it resolved, so a one-off token overwrote the stored login. Shipped in #1431: one resolver, APIFY_TOKEN → stored login, and only apify login writes credentials. The global --token flag in the original plan was dropped — see What shipped.
Part of #1383 (Stage-1). Shipped in #1431, on top of #1417 (Stage-0 pin).
Problem
| Resolver | Order |
|---|---|
resolveToken — utils.ts:132, behind all 46 getLoggedClientOrThrow() calls |
passed token → stored. No env. |
resolveApifyToken — mcp/install.ts:13 |
--token → APIFY_TOKEN → stored |
getApifyTokenFromEnvOrAuthFile — lib/actor.ts:24 |
APIFY_TOKEN → stored |
Same shell, three answers depending on the command. --token exists on 2 of ~50 commands.
Second defect, found in the review of #1293: getLoggedClient() both resolves and persists. Once the resolver returns an env token, setToken() writes it over the stored login and rewrites username/id. The stored account is replaced by a transient env var, and it survives unset.
Decision
APIFY_TOKEN → stored login
This is not a new rule. mcp install and the actor entrypoint already worked this way; only resolveToken did not. It needs no special case for the actor CLI: inside a platform run there is no stored login, so APIFY_TOKEN wins on its own.
The original plan put a global --token in front of APIFY_TOKEN. That was dropped during implementation — see below.
What shipped
Divergences from the plan above, all deliberate:
| Planned | Shipped | Why |
|---|---|---|
Global --token on every command |
Dropped. --token stays on login and mcp install only. |
It duplicated APIFY_TOKEN — two ways to do one thing. gh has no per-command account flag either; you switch with gh auth switch or GH_TOKEN=. Per-command override is APIFY_TOKEN=x apify <cmd>. |
apify login ignores APIFY_TOKEN |
apify login fails when APIFY_TOKEN is set and does not match --token. Exit 5. |
A login that APIFY_TOKEN then shadows reports success for an account no other command uses. That is the #1293 confusion. |
Silent fallback on a bad APIFY_TOKEN |
A placeholder value (undefined, null, 0, …) is a hard error, exit 5. login warns and proceeds, so a broken shell can be fixed. |
Colleague feedback: falling back hides a misconfigured CI variable and runs as the wrong account. |
| No warning when env overrides a login | resolveAuth warns once per command, but only when a stored login exists. |
Silent in CI and platform runs, where APIFY_TOKEN is the only credential and naming it is noise. |
| Two stacked PRs | One PR, two commits in the required order. | Same guarantee, less overhead. |
Also fixed along the way, outside the original scope:
- Stale proxy password on re-login.
loginWithTokenstored a proxy password when the account had one and did nothing when it did not, so logging in as an account without one left the previous account's in the OS keyring.apify runthen handed the Actor the wrong account's proxy credential. Pre-existing, not introduced here. - Unbounded account lookup.
apify runresolves the account before it spawns anything. With the client defaults an unreachable API retried for minutes. NowmaxRetries: 1,timeoutSecs: 10.
Scope as built
- One resolver,
lib/auth.ts.getApifyTokenFromEnvOrAuthFile(lib/actor.ts) andresolveApifyToken(mcp/install.ts) are gone. - Resolve and persist are separate:
resolveAuth()— reads only, single-flighted.APIFY_TOKEN→ stored. Returns the token and its source.loginWithToken(token)— authenticates and saves. The only credential writer in the CLI.
apify auth tokenprints the token that would be used, notgetLocalUserInfo().token.apify infogains atoken sourcerow, so the active credential is visible.apify runpasses the resolved token to the child and no longer overwrites an inheritedAPIFY_TOKEN.- Auth failures name their source and set an exit code:
MissingAuthfor no token,InvalidInputfor a misconfiguredAPIFY_TOKEN.
Commit order
- Write guard — reads stop writing,
loginWithTokenadded, onlyloginpersists. - Precedence — env added to the resolver, the other two resolvers folded in,
runchild env.
Never the reverse. Precedence without the guard is the #1293 data-loss bug.
Resolved questions
--tokenon every command, or only ones that authenticate? Neither. Dropped entirely, replaced byAPIFY_TOKEN=x apify <cmd>.- A one-off token has no stored
username.getCurrentUserInfo()looks it up from the API only when the token came from the environment, caches it per token, and falls back toauth.jsonfor a stored login. - Exit codes on auth failure. Auth paths fixed here. The rest of the CLI stays with Many commands exit with code 0 on failure (and some failures skip telemetry) #1254.
- Warning when env overrides a stored login. Added, conditional on a stored login existing.
Supersedes and related
| Ref | State | Relation |
|---|---|---|
| #720 | open issue | Closes. Asks for APIFY_TOKEN to work as a parameter or env var. |
| #94 | closed issue | The original request, from 2019 — run one command as a different user. Closed into #445. Answered here by APIFY_TOKEN=x apify <cmd>, not by a global flag. |
| #1293 | closed PR | Superseded. First attempt. Correct read-side precedence, blocked because getLoggedClient persisted the env token over the stored login. |
| #1246 | open draft PR | Superseded. Same goal, env-first, no write guard. Close it. |
| #1262 | open issue | Related, not closed. Touches the same line in run.ts, but the hang is the child SDK blocking on a terminal-status POST. The fix may belong in the SDK. |
| #1254 | open issue | Related. Auth paths got correct exit codes here; the rest of the CLI stays with #1254. |
| #1051 | open issue | Related. 401 handling becomes profile-aware in Stage-3 (#1384). |
| #1328 | open epic | Parent discussion. Its "Token handling" section is decided by this issue. |
| apify/agent-skills#80 | cross-repo | Asks to stop using APIFY_TOKEN in their docs. Tell them the outcome — support stays, precedence is now defined, and it outranks a stored login. |
Notes
- No data migration. Behavior change, so it needs a changelog entry.
- Stage-0 (Stage-0: Verify tests and coverage #1387) added a
resolveTokenhappy-path pin first, merged as test: pin auth behavior before the multi-account refactor #1417.
🤖 Generated with Claude Code
Activity
feat: one token resolution order across every command (#1431)