Skip to content

auth.json v2 #1419

Description

@l2ysho

Note

TL;DRauth.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. An older CLI reads v2, finds no token, and reports logged out. Needs a changelog note; the .bak file is the manual way back.

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:101 reads auth.json directly and bypasses getLocalUserInfo. It will silently stop finding the username otherwise.
  • logout calls rimraf on the whole file (logout.ts:29). Route it through the store.
  • Add a test helper for reading a profile. Five test files parse auth.json by 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 run still 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

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