Repository navigation
auth.json v2 #1419
Description
Note
TL;DR — auth.json is a flat blob describing one account, and it holds the whole user('me') response. Multi-account needs a keyed structure, and #567 asked for the same thing before the keyring landed. This replaces the file with { version, activeProfile, profiles: Record<userId, Data> }, migrates the three states in the wild, and trims the stored fields. No UX change — the file can hold N profiles, nothing puts a second one there yet.
Part of #1383 (Stage-1). Closes #567, except its listing part — see below.
Shape
{
"version": 2,
"activeProfile": "<userId>",
"profiles": {
"<userId>": {
"username": "moria",
"name": null, // reserved for Stage-3 `--profile <name>`
"organizationOwnerUserId": "…", // set => this profile is an organization
"authMethod": "token", // reserved, unused until Stage-4
"expiresAt": null, // reserved
"hasRefreshToken": false // reserved
}
},
"secretsBackend": "keyring"
}Keyed by userId, not by a display name. The key names the keyring entry in the next subtask, so a rename must never orphan a secret.
The reserved fields stay unused. authMethod, expiresAt and hasRefreshToken are for the device flow, which issues expiring access tokens when the OAuth app opts into refresh tokens. name is for Stage-3, where a profile gets a human label. Leaving any of them out forces a second migration later.
No proxy object in the profile. The password is a secret and belongs to #1420; nothing reads proxy.groups. On the keyring backend today's code already strips the password and deletes the empty object (utils.ts:203-208).
Fields
Today getLoggedClient does { ...existingFile, ...userInfo }, so the file holds email, plan, effectivePlatformFeatures, isPaying, createdAt and proxy groups. None of them are read anywhere — email is even declared in the AuthJSON type and never used.
Across the 20 getLocalUserInfo() call sites the CLI consumes username, id, token and proxy.password, plus organizationOwnerUserId for labeling organization profiles later. Drop the rest, and drop email from AuthJSON.
Migration
Three states exist in the wild:
| State | auth.json | keyring |
|---|---|---|
| A | plaintext token + proxy.password, no marker |
empty |
| B | no secrets, marker keyring |
token, proxy-password |
| C | plaintext secrets, marker file |
empty |
ensureMigrated() already turns A into B or C. The v2 shape migration runs after it, as a separate step — do not merge the two functions.
- back up the old file as
auth.json.v1.bak, and never overwrite an existing backup - write via temp file + rename; two CLI processes can run at once
- idempotent, single-flight, never throws — a migration failure must not block a command
- no reverse migration, but an older CLI is not broken by one either. Verified against
apify-cli@1.10.0: it finds the token — top-level on the file backend, via the survivingsecretsBackendmarker on keyring — and its per-command auth.json refresh re-adds the flatid/username, so it self-heals and the file ends up carrying both shapes. No changelog note needed for downgrade. The.bakis still written, and still holds a plaintext token on the file backend.
Edge cases
| Case | Behavior |
|---|---|
activeProfile names a profile that is not in profiles |
treat as logged out, and say which profile is missing |
version higher than 2 |
clear error — the file was written by a newer CLI. Do not migrate backwards |
v1 file has a token but no id |
no key to store it under. utils.ts:113 already calls this stale. Keep the .bak, require a re-login, do not invent a key |
| two commands write at once | each write is atomic through temp file + rename, but a read-modify-write pair still races. Last write wins. This is accepted, not a bug to lock around |
Also in scope
getLocalUserInfo()keeps its current return shape, reading the active profile. That keeps the 20 call sites untouched.useRentalSunsetNotice.ts:101readsauth.jsondirectly and bypassesgetLocalUserInfo. It will silently stop finding the username otherwise.logoutcallsrimrafon the whole file (logout.ts:29). Route it through the store.- Add a test helper for reading a profile. Five test files parse
auth.jsonby hand today:run.test.ts(×3),log_in_out.test.ts,info.test.ts,actors/search.test.ts,rental-sunset-notice.test.ts.
Not in scope
No additive login, no --profile, no switch, no list. #567 also asks to print all logged-in accounts — that is auth list, Stage-3 (#1384).
Verification
- A → v2, B → v2, C → v2
- no file → login creates v2
- all of the above with
APIFY_DISABLE_KEYRING=1 - migration is a no-op on a file that is already v2
apify runstill gets the right token and proxy password (run.test.ts:128,:169,:209)- logout removes the profile
- bundle build still loads the native keyring module
🤖 Generated with Claude Code
Activity
feat: auth.json v2 with profiles keyed by user ID
feat: auth.json v2 with profiles keyed by user ID
feat: auth.json v2 with profiles keyed by user ID (#1434)