Conversation
l2ysho
force-pushed
the
claude/auth-json-v2-1419
branch
from
September 23, 2026 09:14
d903722 to
0452abf
Compare
l2ysho
force-pushed
the
claude/secret-storage-v2-1420
branch
2 times, most recently
from
September 24, 2026 12:38
a39ad9f to
95f8bfd
Compare
Secrets lived under one fixed name per kind, so a second account would overwrite the first one's token. Both the keyring and the file backend are now keyed by user ID, and existing secrets are re-keyed in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A keyring write that fails for one account used to flip the file-level marker, sending every other account to a file that does not hold their secrets. The fallback is now recorded on the profile that hit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
l2ysho
force-pushed
the
claude/secret-storage-v2-1420
branch
from
September 24, 2026 14:08
95f8bfd to
85ac3fd
Compare
auth.json is the only index of what the keyring holds, so both commands destroyed entries before a step that can throw. A failed account switch left the outgoing account's token deleted and the new one unwritten, and a failed logout destroyed the secrets while auth.json still named the account. Login now clears after the switch is on disk; logout attempts both steps and reports what is left behind instead of claiming success. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Secret reads are v2-only, so a failed shape migration leaves the CLI unable to read the token. It still told the user their login worked, and the next command said they were not logged in. The warning now states what is true and how to fix it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
auth.json is replaced through a temp file and a rename, so the write needs the directory to be writable and the file's own mode never matters. Telling the user to make the file writable sends them to change something that has no effect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch has not been deployed
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 — in place. No UX change.
Stacked on #1434 (Stage-1, subtask 4). Base branch is
claude/auth-json-v2-1419, notmaster.Closes #1420. Part of #1383.
Change
com.apify.cli/token,proxy-passwordcom.apify.cli.token,com.apify.cli.proxy-password/<userId>auth.json.token,auth.json.proxy.passwordprofiles[<userId>]Service per kind, not a composite account.
token:<userId>under one service would depend on:being legal in an account name on macOS Keychain, libsecret and Windows Credential Manager, and it reads worse in Keychain Access.getToken/setToken/getProxyPassword/setProxyPassword/deleteProxyPasswordcollapse intogetSecret(userId, kind),setSecret(userId, kind, value)anddeleteSecret(userId, kind). One implementation, and the key is visible at every call site.Both backends in one PR, because a keyring write can fail and send that account to the file mid-run. Split across two releases, that fallback would write the secret under a name the next read does not look for.
AuthProfile.secretsBackendgoes liveThis branch reserved the field for the moment secrets became per-profile. That is here, so it is now written and read.
backendFor(userId)is the profile's ownsecretsBackend, falling back to the file-level one. Every read and write goes through it.secretsBackend: 'file'on that profile, in the same write as the secret, and leaves the file-level marker alone. Previously it flipped the file-level marker, which would send every other account to a file that does not hold their secrets — exactly what the field's comment warned about.downgradeBackendToFile()survives with one caller,ensureMigrated(), which runs before any profile exists — there the file is the only place a fallback can be recorded.Migration
ensureSecretsKeyed()is a separate step from the two that already exist. A v2 file whose secrets still sit under the old names is a supported state — every user is in it between #1434's release and this one — so the two migrations stay independent.It runs after
ensureAuthFileCurrent(), which is where the user ID comes from.resolveAuth()keeps this branch's deliberate split —ensureMigrated()early, the shape migration only onceAPIFY_TOKENhas been ruled out — so the new step sits with the second half rather than being folded into one helper.Behavior worth calling out
logoutreordered.auth.jsonis the only index of what the keyring holds, so removing the profile first would strand its entries. The keyring is cleared first now. Legacy fixed-name entries are still deleted too, so logout works for anyone who never re-keyed. No version guard needed —removeActiveProfile()already discards a file it cannot read rather than refusing.replaceStoredAccount()drops the previous profile and the secrets stored in the file, but keyring entries under the old user's key would survive and become unreachable —logoutonly ever clears the active profile. Two regression tests cover the switch and the same-account re-login.idhas no key to file the secret under. The secret is dropped and the next command asks for a re-login. That state already required a re-login — there is no account to attach the token to — so nothing reachable is lost. The v1 backup does not hold it either: this branch deliberately strips secrets fromauth.json.v1.bak.getLocalUserInfo()'s "Stale credentials found without user metadata" branch is gone. It was the error for exactly the state above, and dropping the secret makes it unreachable. A danglingactiveProfilenow reports the missing profile by name whether or not a secret was found — previously it could only tell when a token happened to be readable.auth.jsonstill strands keyring entries. The keyring has no listing API, and reaching for fixed names on a machine with no account would touch the keyring on every command. Asserted in a test so the trade-off is on the record.Verification
pnpm run test:local— 675 passed, 4 skipped (63 files), on this branch rebased onto the current base. Net +17 test cases (30 added, 13 replaced).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.${service}:${account};com.apify.cli.token:uidandcom.apify.cli:tokenare distinct strings, so the new names do not collide with the legacy ones in that map.APIFY_DISABLE_KEYRINGtoggled between login and logout; logout leaving other profiles alone; two accounts holding their own entries;deleteSecrettouching only one account and kind; idempotency and single-flight.Not tested by hand: the macOS Keychain prompt. Creating an item under a new service may prompt, and this migration runs on the first command after the upgrade. Worth one manual check before release; if it prompts, re-key at next login instead of at first command.
Left out
--profile, noauth switch, noauth list.clearKeyringSecrets()takes the profile whose entries to clear, but only ever sees one profile today — Stage-2 (Stage-2: Login - token multi account support #1386) is what puts a second one there.🤖 Generated with Claude Code