Conversation
`getLoggedClient()` both resolved a token and persisted it. Any command given a token other than the stored one overwrote the stored login with it, and rewrote `username`/`id` to match, so a transient token replaced the saved account. Splits the pair: - `resolveAuth()` in the new `src/lib/auth.ts` reads only. Order is `--token` flag, then stored login. - `loginWithToken()` authenticates and saves. Only `apify login` calls it, and it now replaces the stored account instead of merging into it, so fields the new account lacks cannot linger from the old one. - `getLoggedClient()` verifies the token and returns a client. It writes nothing. Commands that read `username`/`id` to address API resources now go through `getCurrentUserInfo()`, which returns auth.json for a stored token and the account behind a one-off token otherwise. They previously relied on `getLoggedClient()` refreshing auth.json, which is the write that just went away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
l2ysho
force-pushed
the
claude/token-resolution-1418
branch
from
September 11, 2026 10:15
9bde8e0 to
da465e5
Compare
Three resolvers decided which token a command used, and they disagreed:
`resolveToken` ignored `APIFY_TOKEN`, `mcp install` honored it, the `actor`
entrypoint required it. The same shell gave three answers depending on the
command.
There is now one order, the one `mcp install` and the `actor` entrypoint already
used:
token the command was given -> APIFY_TOKEN -> stored login
`APIFY_TOKEN` is the way to run a command as a different account. Only `login`
and `mcp install` take a token of their own, through `--token`, because both
write it somewhere rather than just authenticating with it. Adding a second,
flag-shaped way to do what the env var already does would leave two mechanisms
for one thing.
- `getApifyTokenFromEnvOrAuthFile` and `resolveApifyToken` are gone, folded into
`resolveAuth`. No special case is needed for the `actor` entrypoint: inside a
platform run there is no stored login, so `APIFY_TOKEN` wins on its own.
- `apify auth token` prints the token that would be used, not the stored one. It
only looked right before because reads overwrote the stored token.
- `apify info` names the source, so an `APIFY_TOKEN` that overrides a stored
login is visible rather than silent.
- `apify run` passes the resolved token to the child instead of the stored one,
and no longer overwrites an inherited `APIFY_TOKEN`.
- A rejected token names its source. A 401 or 403 says the token was rejected;
any other failure says the API request failed, so an unreachable API is not
reported as a bad token.
- `apify login` ignores `APIFY_TOKEN`. Logging in stays explicit.
Closes #720
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
l2ysho
force-pushed
the
claude/token-resolution-1418
branch
2 times, most recently
from
September 11, 2026 12:39
4f8bf65 to
09d33ff
Compare
Now that `APIFY_TOKEN` beats the stored login, the override is silent, and a variable that was never really set is indistinguishable from a real token. - A placeholder value falls back to the stored login instead of failing against a token that does not exist. `APIFY_TOKEN=$UNSET_VAR` leaves an empty string and templating an absent value writes the literal "undefined"; both are now ignored, the first silently and the second with `APIFY_TOKEN is invalid: "undefined".` The value is also trimmed. - When `APIFY_TOKEN` overrides a stored login, the CLI says `Using the API token from APIFY_TOKEN.` once per command. It stays quiet when the env var is the only credential, which is the case in CI and inside a platform run, so this adds no noise there. - `apify login` warns that `APIFY_TOKEN` will keep winning. Without it the success message is a lie: commands would run as whoever owns the env token, not as the account just logged in. - `apify logout` warns that `APIFY_TOKEN` still authenticates. Logging out does not unset an env var. Every notice goes to stderr, so `apify auth token` stays pipeable and `--json` output stays parseable. A token the API rejects with 401 or 403 does not fall back. Running `APIFY_TOKEN=$CUSTOMER apify push` with an expired customer token would then push into your own account, which is the silent wrong-account write this branch removes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
l2ysho
force-pushed
the
claude/token-resolution-1418
branch
from
September 11, 2026 13:43
09d33ff to
1e7e56c
Compare
Review of the two commits above found `apify run` had gained a blocking, retried API call. `getCurrentUserInfo()` runs before the child Actor is spawned, and with `APIFY_TOKEN` set it reaches the API. apify-client defaults to 8 retries at 360s each, and `_get` passes no per-request timeout, so an unreachable API stalled the run: 208s measured against a refused connection, and roughly an hour against one that accepts the socket then hangs. The `.catch()` made that failure non-fatal but not fast. - The cold-path lookup gets `maxRetries: 1, timeoutSecs: 10`. Measured 208s to 3s. The child keeps the right token and only loses `APIFY_USER_ID` and the proxy password, which is the correct degradation. - `getCurrentUserInfo()` reads the cache before falling back to auth.json. `getLoggedClient()` already fetched fresh account data under the same token, and the stored path threw it away. This closes the staleness gap the previous commit accepted: a username change no longer breaks name resolution until the next login. The file fallback stays for a cold cache, so `apify run` offline on a stored login still works. - `actors pull` resolves the client before the account, like every other command. It was first, so a rejected token surfaced a raw `ApifyApiError` instead of the message naming the source. - `actors search` is deliberately anonymous. It resolved auth only to delete the token, which made it announce "Using the API token from APIFY_TOKEN." for a request that carries none, and cost a keyring read. It now takes `getAnonymousApifyClientOptions()`, which never resolves. - `describeAuthFailure` narrows on `instanceof ApifyApiError` instead of duck-typing `statusCode`. The tests built the error by hand, so nothing would have noticed if that field moved and every rejected token started reporting "the API request failed" — the misdiagnosis the branch exists to prevent. - `ensureMigrated()` runs again regardless of token source. It was reachable only on the stored fallthrough, so anyone always exporting `APIFY_TOKEN` would never migrate a legacy plaintext auth.json into the keyring. - Drops `delete fileContents.token`: `User` carries no token, and the file is replaced rather than merged. Adds the `getCurrentUserInfo()` tests that were missing: the stored path, the env path, the cache hit, and the rename it now picks up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review feedback: a warning is easily lost in noisy CI and Actor logs, and both of these almost always mean a broken script or a misconfigured workflow, so an error is what the caller can act on. - A placeholder `APIFY_TOKEN` fails with exit code `InvalidInput` rather than quietly falling back to the stored login. This supersedes the fallback added two commits ago: a placeholder and a revoked token are the same failure, the operator meant to act as one account and the CLI was about to act as another, and only one of them stopped. - Blank and whitespace-only values still fall back without a word. They mean the variable was never set, not that it was set wrong. - `apify login` fails when `APIFY_TOKEN` holds a different token, because the success line would name an account no later command uses. The same token is the CI idiom of setting `APIFY_TOKEN` and also running `apify login --token $APIFY_TOKEN`, where nothing is wrong, so that stays silent. The guard sits at the top of `run()`: the browser flow only learns its token from the Console callback, and failing there would mean failing after the user has already authorized. - `apify logout` keeps its warning. It did what it says. `resolveAuth` keeps its early returns, so `--token` still short-circuits ahead of the env var and a broken `APIFY_TOKEN` cannot lock you out of logging in to fix it. Both escape hatches have tests. Splits `getApifyClientOptions` in two. Four call sites resolved a token and then handed it back to the resolver, which returned it unchanged as `source: 'flag'` and discarded the source. They now take `getApifyClientOptionsForToken`, which resolves nothing. The old form had no remaining caller and is gone, along with three tests of it that `auth.test.ts` already covers; the fourth moves to `resolveAuth`, being the only check that resolving still triggers the keyring migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`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. `getLocalUserInfo` re-attaches it, and `apify run` exports it to the child as `APIFY_PROXY_PASSWORD` — the Actor ran with the wrong account's proxy credential. auth.json is replaced wholesale on login, so the metadata could not linger this way; the keyring outlives that rewrite and needed clearing of its own. Adds `deleteProxyPassword()` for it, backend-aware so the file path stays correct too, called only when there is nothing to store — deleting on every login would cost a keyring write, which is what `skipIfUnchanged` exists to avoid. Pre-existing, identical on the base branch, but the write guard's own comment claims fields the new account lacks cannot linger. Now that holds for both backends. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Findings from a reuse/simplification/efficiency/altitude pass.
`getEnvToken()` mapped both "unset" and "placeholder" to undefined, so
`login` and `logout` could not see a value every other command rejects.
`APIFY_TOKEN=undefined apify login --token <real>` therefore reported
success, and the next command hard-failed on the same variable.
`readEnvToken()` returns unset / invalid / token instead, so all three
callers branch on the same cases. That also retires the raw second read
of `process.env` the resolver used to recover the distinction it had just
discarded.
`login` warns rather than aborting on an invalid value: every other
command fails, so it is the only way back, and refusing it would lock
someone out of the fix.
`resolveAuth` is now single-flighted like `getBackend`. It ran two to
four times per command, each re-reading an uncached OS keyring entry —
measured at 3 reads for `apify info`, 4 for `apify run`. One resolution
per process also removes the once-per-notice boolean and its test-only
reset, which existed only to make repeated resolution look single, and
takes the `existsSync` behind it off the hot path.
- `describeAuthFailure` takes the `ResolvedAuth` its caller already held
instead of resolving again, so it is synchronous and the message is
worded from the resolution that actually failed.
- `resolveAuth` drops its token parameter. `mcp install` was the only
caller passing one, and nothing read the resulting `'flag'` source, so
the union member, its label and its switch branch were unreachable.
That precedence now sits at the one call site that wants it.
- `getApifyStorageClient` routes through `describeAuthFailure` rather
than carrying a fourth wording for "no token".
- The three test-only cache resets become one `resetAuthCaches()`, called
at the three hook sites instead of nine calls.
- `run.ts` drops a `{} as AuthJSON` cast that asserted a shape the value
did not have; every field is optional, so the catch is typed instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- `stripProxyPassword` in `credentials.ts` replaces three copies of "drop `proxy.password`, drop `proxy` when that was all it held" — two added by this branch, one pre-existing in `ensureMigrated`. - `loginWithToken` returns the account it fetched, so `login` stops re-reading auth.json for a username it was just handed. That also drops its exposure to `getLocalUserInfo`'s stale-credentials throw on a path that had just succeeded. - `auth token` and `info` lose `if (auth)` guards that could not be false: the `getLoggedClientOrThrow()` above each proves a token resolved. `info` no longer builds an optional-valued map and loops its keys to print three fixed rows. - The fake `apify-client` lived verbatim in both auth test files and had already drifted by a field. It moves to `test/__setup__/apify-client-mock.ts`, next to `keyring-mock.ts`, which is the pattern this repo already uses for a shared module mock. - The single-assertion `resolveAuth()` describe left in `credentials.test.ts` folds into the `ensureMigrated()` suite, where the behavior it checks belongs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
TL;DR — TBD
Stacked on #1417 (Stage-0). Base branch is
claude/github-issue-1387-d39d52, notmaster.Checklist
skills/apify/SKILL.mdafter the implementation landsBehavior, before and after
Run against two real accounts, account-a (logged in) and account-b (
$B), with 1.10.0 side by side.APIFY_TOKENno longer ignoredAPIFY_TOKEN=$B apify actors lsAPIFY_TOKEN=$B apify actors ls(logged out)Error: You are not logged inAPIFY_TOKEN=$B apify infousername: account-ausername: account-b+token source: APIFY_TOKEN environment variableAPIFY_TOKEN=$B apify auth tokenAPIFY_TOKEN=$B apify actors info <name>account-a/<name>→ not foundaccount-b/<name>→ foundapify runhanded the child the wrong identityAPIFY_TOKEN=$B apify runThis is the "Insufficient permissions for the Actor run" report, and why it started working after
apify logout.Reads overwrote the stored login
APIFY_IS_AT_HOME=1 APIFY_TOKEN=$B apify actor charge result-itemauth.jsonnow holds account-b. UnsettingAPIFY_TOKENdoes not restore account-aauth.jsonuntouchedA misconfigured
APIFY_TOKENfails instead of guessingAPIFY_TOKEN=undefined apify actors lsError: APIFY_TOKEN is set to "undefined", which is not an API token.exit 5APIFY_TOKEN= apify actors lsAPIFY_TOKEN=<revoked> apify actors lsError: The API token in APIFY_TOKEN was rejected.APIFY_TOKEN=$B apify login --token $ASuccess: logged in as account-a, while every later command ran as account-bError:exit 5. Same token stays silent — that is the CI idiomAPIFY_TOKEN=undefined apify login --token $Aapify login --token <bad>login && pushran onWhat changed
resolveAuth()in the newsrc/lib/auth.ts.getApifyTokenFromEnvOrAuthFileandresolveApifyTokenare gone. No special case for theactorentrypoint: inside a platform run there is no stored login, soAPIFY_TOKENwins on its own.loginWithToken()is the only credential writer, and it replaces the stored account rather than merging into it — including the keyring proxy password, which used to survive a re-login and leak the wrong account's credential intoapify run's child.resolveAuthis single-flighted. It ran 2–4× per command, each an uncached OS keyring read.APIFY_TOKEN, returning unset / invalid / token, sologinandlogoutsee a value every other command rejects.usernamemoved togetCurrentUserInfo(), which reads the resolved account rather thanauth.json.--tokenstays onloginandmcp installonly. No global flag —APIFY_TOKENalready does that job, and two mechanisms for one thing is worse.Closes #1418. Closes #720.
Verification
pnpm run test:local— 621 passed, 4 skipped.APIFY_TOKENset to a bogus value, which is deliberate: a host token must not decide which account the tests run as.update-docsclean.pnpm run test:apinot run — no token in this environment.Upgrade note
A stale
APIFY_TOKENin a shell profile or CI job now decides which account commands run as. If it holds a placeholder, commands fail rather than falling back. Worth a release note.🤖 Generated with Claude Code