Conversation
auth.json was a flat blob describing one account, holding the whole
user('me') response. It is now { version, activeProfile, profiles,
secretsBackend }, so it can hold N accounts. Nothing puts a second one
there yet, and users see no change.
New src/lib/auth-file.ts owns the file: reading, an atomic write, the
v1 to v2 migration, and the profile accessors. credentials.ts, login,
logout, getLocalUserInfo() and the rental notice all go through it.
The migration backs the old file up as auth.json.v1.bak, runs after
ensureMigrated() as a separate step, is idempotent and single-flight,
and never throws. Fields nothing reads are dropped: email, plan,
effectivePlatformFeatures, isPaying, createdAt and proxy.groups.
Closes #1419
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
l2ysho
force-pushed
the
claude/auth-json-v2-1419
branch
from
September 17, 2026 11:46
7a941e1 to
cc85096
Compare
Both parsed auth.json by hand and asserted the v1 flat shape, so neither
could pass against a v2 file. log_in_out deep-equalled the file against
the whole user('me') response, which v2 deliberately no longer stores;
info read a top-level id that is now the profile key.
Both now read the active profile through the test helper, and
log_in_out checks the token through getToken() rather than the file.
Not run here — test:api needs a live token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
copyFileSync inherits the source mode. An auth.json written before the CLI started passing mode 0600 is still 0644, and writeFileSync's mode applies only on create, so it stayed that way. The new atomic write fixes auth.json on the first v2 write, but the backup is copied before that and never rewritten — leaving a plaintext token at 0644. Also fixes two tests: apify info prints three rows since the token source line landed, and the idempotency check called the migration twice without resetting the memoised promise, so the second call never touched the file. Adds the missing cover for logout removing the backup, which is the only path that erases that token from disk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The backup is written once and never refreshed, and only logout deletes it. So after `apify login` as a second account, auth.json holds the new token while auth.json.v1.bak still holds the previous one — for as long as the user never logs out. Nothing reads the backup, and a downgraded CLI finds its token through the keyring or auth.json rather than here, so the secrets are dropped when writing it. Also pins the two lines that make the migration run for users. Deleting `await ensureAuthFileCurrent()` from either resolveAuth() or getLocalUserInfo() left the whole suite green: every migration test called it by hand. 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 #1431 (Stage-1, subtask 2). Base branch is
claude/token-resolution-1418, notmaster.What changed
New
src/lib/auth-file.tsowns the file — reading, writing, migrating, and the profile accessors.credentials.ts,login,logout,getLocalUserInfo()and the rental-sunset notice all go through it, so no caller parsesauth.jsonby hand any more.{ "version": 2, "activeProfile": "<userId>", "profiles": { "<userId>": { "username": "moria", "name": null, // reserved for `--profile <name>` "organizationOwnerUserId": "…", // set => organization profile "authMethod": "token", // reserved, device flow "expiresAt": null, // reserved "hasRefreshToken": false // reserved } }, "secretsBackend": "keyring" }userId, not a display name. The key names the keyring entry in Secret storage v2 #1420, so a rename must never orphan a secret.token/proxy.passwordon the file backend. Per-profile secret keys are Secret storage v2 #1420; putting them in the profile now would force a second migration.email,plan,effectivePlatformFeatures,isPaying,createdAt, andproxy.groups.emailis also gone from theAuthJSONtype, where it was declared and never used.getLocalUserInfo()keeps its return shape, so its call sites are untouched.0600. Two CLI processes can run at once. A read-modify-write pair still races and last write wins; that is accepted, not locked around.Migration
Runs after
ensureMigrated()as a separate step, so a keyring failure and a shape failure cannot mask each other. Idempotent, single-flight, and it never throws — a migration failure must not block a command.ensureMigrated()put themkeyringtoken,proxy-passwordfileauth.json.v1.bak, and an existing backup is never overwritten.idhas no key to store a profile under. The secrets and the.bakare kept and the next command asks for a re-login; no key is invented.versionis higher than 2 is refused with a clear error on read, and by both profile writers. No reverse migration.Behavior worth calling out
apify logoutroutes through the store instead ofrimraf-ing the file. It drops the active profile and its plaintext secrets, and removes the file once no profile is left — same observable result today, and it does not need rewriting whenauth switcharrives. It also removesauth.json.v1.bak, which would otherwise leave a plaintext token on disk after a logout.apify-cli@1.10.0on the file backend:apify infoprinted the right username and user ID. It finds the top-leveltoken, and its per-command auth.json refresh — the write feat: one token resolution order across every command #1431 removes — merges theuser('me')response back in, restoring the flatid/usernameit needs. The file then carries both shapes and both CLIs read it. On the keyring backend thesecretsBackendmarker survives the migration, so the same path should hold; not verified, no keyring entry on the test machine. No downgrade release note needed.Deviation from the issue
The issue specified that a dangling
activeProfileshould be "treated as logged out". It now throws a clear error naming the missing profile when a token is present, and reports logged out only when there is nothing to authenticate with. Returning{}whileresolveAuth()still resolves the top-level token left ~9 commands building`${undefined}/${name}`and failing with a misleadingActor with name "x" was not found— and the adjacent "noactiveProfileat all" state, which a user cannot tell apart, already threw. Only reachable from a hand-edited file; no writer produces it.Verification
pnpm run test:local— 634 passed, 4 skipped (63 files), up 3 from the base.pnpm run lint,pnpm run format,pnpm run build— clean.pnpm run update-docs— no change; no flag, arg, description, or registration moved.pnpm run test:apinot run — no token in this environment.log_in_out.test.tsandinfo.test.tsasserted the v1 flat shape and could not have passed against a v2 file; both were updated, but only by reading, not by running.test/local/lib/auth-file.test.ts: A/B/C on both backends, idempotency on an already-v2 file, the backup guard, the dropped fields, a corrupt file, a v1 file with noid, a danglingactiveProfile, and aversion: 3file surviving both a login and a logout byte-for-byte.test/__setup__/auth-file.tshelper. Thetest/local/files that parsedauth.jsonby hand now read a profile through it or throughgetLocalUserInfo(), as do the two API tests that asserted the v1 shape.package.jsonandpnpm-lock.yamlare identical to the base branch).Requires Node ≥22 to run pnpm 11 locally.
Left out, deliberately
--profile, noauth switch, noauth list. The file can hold N profiles; Stage-2 (Stage-2: Login - token multi account support #1386) puts a second one there. Figure out a better auth file structure for future extensibility #567's listing part isauth list, Stage-3 (Stage-3: Multi-account UX #1384).proxy.groups.Closes #1419
🤖 Generated with Claude Code