Skip to content

Token resolution #1418

Description

@l2ysho

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. loginWithToken stored 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 run then handed the Actor the wrong account's proxy credential. Pre-existing, not introduced here.
  • Unbounded account lookup. apify run resolves the account before it spawns anything. With the client defaults an unreachable API retried for minutes. Now maxRetries: 1, timeoutSecs: 10.

Scope as built

  • One resolver, lib/auth.ts. getApifyTokenFromEnvOrAuthFile (lib/actor.ts) and resolveApifyToken (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 token prints the token that would be used, not getLocalUserInfo().token.
  • apify info gains a token source row, so the active credential is visible.
  • apify run passes the resolved token to the child and no longer overwrites an inherited APIFY_TOKEN.
  • Auth failures name their source and set an exit code: MissingAuth for no token, InvalidInput for a misconfigured APIFY_TOKEN.

Commit order

  1. Write guard — reads stop writing, loginWithToken added, only login persists.
  2. Precedence — env added to the resolver, the other two resolvers folded in, run child env.

Never the reverse. Precedence without the guard is the #1293 data-loss bug.

Resolved questions

  • --token on every command, or only ones that authenticate? Neither. Dropped entirely, replaced by APIFY_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 to auth.json for 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

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    t-buildersIssues owned by the Builders team.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions