Skip to content

fix(auth): stop parallel commands from logging the user out - #674

Closed
amitbardos wants to merge 1 commit into
mainfrom
amitb/fix-parallel-refresh-logout
Closed

amitbardos wants to merge 1 commit into
mainfrom
amitb/fix-parallel-refresh-logout

Conversation

@amitbardos

@amitbardos amitbardos commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Note

Description

Running multiple Base44 CLI commands in parallel (common when agents drive the CLI) could log the user out entirely. Refresh tokens rotate on use, so when several processes each found an expired access token, they all refreshed with the same refresh token — every refresh after the first was rejected, and the blanket catch in refreshAndSaveTokens() deleted ~/.base44/auth/ for all of them. This PR adds a cross-process lock file around the refresh and narrows auth deletion to the one case that actually means "log in again".

Related Issue

None

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Other (please describe):

Changes Made

  • Added withRefreshLock() in core/auth/config.ts: acquires ~/.base44/auth/auth.json.lock with open(path, "wx"), retrying every 50ms, and reclaims a lock whose mtime is older than 30s (left by a process that died mid-refresh). The lock is always released in a finally.
  • refreshAndSaveTokens() now snapshots the refresh token before taking the lock and re-reads auth.json after acquiring it. If the refresh token changed, another process already refreshed, so it returns that process's access token instead of burning the rotated token.
  • Narrowed the failure path: auth is deleted only when the server rejects the refresh token (ApiError with status 400 or 401). Network failures and 5xx responses now return null and leave the credentials on disk.
  • The existing in-process refreshPromise guard is kept and its comment clarified — it only dedupes within a single process; the lock file covers the cross-process case.
  • Updated docs/api-patterns.md to document the lock file, the re-read-after-lock behavior, and the narrowed deletion rule (including the env-seeded auth section, which referenced the old "refresh fails to delete" wording).

Testing

  • I have tested these changes locally
  • I have added/updated tests as needed
  • All tests pass (npm test)

New test in packages/cli/tests/cli/authorization.spec.ts: two agents push processes run concurrently against a mock /oauth/token route that honors the refresh token exactly once and then returns 400 invalid_grant. The first refresh is held until the second process arrives (or 2s elapse), forcing the race. Both commands must succeed and auth.json must end up holding the rotated refresh token. Per the author, this fails 3/3 on main and passes 10/10 with the fix.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have updated docs/ (AGENTS.md) if I made architectural changes

Additional Notes

The lock is advisory and file-based, so it works across independent npx base44 processes on the same machine without any new dependency. Stale-lock reclamation is time-based (30s), which is the usual trade-off: a refresh that legitimately takes longer than 30s could have its lock stolen, degrading to the pre-existing behavior rather than making anything worse.

The full suite was not run in this review environment (test execution was not permitted here), so the "all tests pass" box is left unchecked pending CI.


🤖 Generated by Claude | 2026-10-22 12:55 UTC | c653912

Refresh tokens rotate on use. When several CLI processes found an expired
token at once, each refreshed with the same refresh token; every refresh
after the first was rejected and its catch deleted ~/.base44/auth for all
of them. The in-memory refreshPromise only guarded a single process.

Hold a lock file across processes while refreshing, and use the token
another process already wrote instead of refreshing again. Delete auth only
when the server rejects the refresh token, not on network or server errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.2.2-pr.674.03ac2f7

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.2.2-pr.674.03ac2f7"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.2.2-pr.674.03ac2f7"
  }
}

Preview published to npm registry — try new features instantly!

@amitbardos amitbardos closed this Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant