Skip to content

feat(oauth): add rotating refresh tokens - #408

Open
xizhibei wants to merge 6 commits into
mainfrom
codex/issues-oauth-403-404
Open

feat(oauth): add rotating refresh tokens#408
xizhibei wants to merge 6 commits into
mainfrom
codex/issues-oauth-403-404

Conversation

@xizhibei

@xizhibei xizhibei commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • allow validated RFC 8252 loopback consent redirects through the restrictive consent-page CSP
  • implement persistent, digest-only, rotating refresh-token families with replay revocation and scope/resource/client binding
  • add provider/storage coverage and a real Chromium DCR, consent, PKCE, refresh, and rotation flow

Validation

  • pnpm lint
  • pnpm typecheck
  • pnpm build
  • pnpm test:unit (297 files, 4,334 tests)
  • pnpm test:e2e test/e2e/oauth-loopback-consent.e2e.test.ts (Chromium, 1 test)

Existing Vitest mock-hoisting and Node deprecation warnings remain unchanged.

Closes #403
Closes #404

Summary by CodeRabbit

  • New Features
    • Added OAuth refresh-token rotation via single-use refresh “families” with fixed 30-day lifetimes, including scope narrowing, client/resource validation, and replay revocation.
    • Consent pages now surface “renewable access” only for clients supporting refresh tokens.
  • Documentation
    • Added/updated refresh-token rotation guidance (English and Chinese) and updated the related ADR status.
  • Bug Fixes
    • Hardened OAuth exchange/verification with clearer error handling, improved durable persistence, and safer exclusive locking/recovery for stale artifacts.
    • Tightened consent-page CSP form-action to a validated loopback origin.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@xizhibei, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 163bc416-3ada-4798-9d67-da7b2613595f

📥 Commits

Reviewing files that changed from the base of the PR and between 1c55765 and 6bd2af4.

📒 Files selected for processing (2)
  • src/auth/storage/fileStorageService.test.ts
  • src/auth/storage/fileStorageService.ts
📝 Walkthrough

Walkthrough

OAuth authorization now issues and rotates durable refresh-token families for eligible clients. Family state uses digests, exclusive locks, runtime isolation, scope/resource validation, replay revocation, and linked access-session checks. Consent-page loopback CSP handling and broad validation coverage were added.

Changes

OAuth rotating refresh-token families

Layer / File(s) Summary
Refresh-family contracts and runtime configuration
src/auth/sessionTypes.ts, src/auth/oauthAuthorizationFlow.ts, src/auth/storage/sessionRepository.ts, src/auth/storage/oauthStorageService.ts, src/constants/auth.ts, src/core/runtime/runtimeIdentityService.ts, src/transport/http/server.ts
Refresh-family schemas, session linkage, storage signatures, runtime-scope access, repository wiring, and refresh-family file configuration were added.
Durable writes and exclusive storage locks
src/auth/storage/fileStorageService.ts, src/auth/storage/fileStorageService.test.ts
File storage now supports schema validation, atomic durable replacement, temporary-file cleanup, lock acquisition, stale-lock recovery, and lock cleanup tests.
Refresh-family repository and session persistence
src/auth/storage/refreshTokenFamilyRepository.ts
Refresh-token digests, lookup records, lineage, rotation, replay state, runtime isolation, access-session checks, and family revocation persistence were implemented.
Authorization, rotation, revocation, and consent flow
src/auth/sdkOAuthServerProvider.ts
Authorization-code issuance conditionally creates families, refresh exchanges rotate tokens, replay revokes family sessions, access verification checks family state, typed errors are used, and consent CSP handling validates loopback origins.
Validation and documentation
src/auth/sdkOAuthServerProvider.refresh.test.ts, src/auth/sdkOAuthServerProvider.test.ts, test/e2e/*, docs/en/guide/advanced/authentication.md, docs/zh/guide/advanced/authentication.md, docs/adr/0011-oauth-refresh-tokens-use-rotating-families.md
Tests cover persistence, scope and resource constraints, replay, revocation, concurrency, browser consent flows, and storage failures; documentation records the accepted rotating-family design.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OAuthClient
  participant SDKOAuthServerProvider
  participant RefreshTokenFamilyRepository
  participant SessionRepository
  OAuthClient->>SDKOAuthServerProvider: exchange authorization code
  SDKOAuthServerProvider->>RefreshTokenFamilyRepository: create refresh family
  SDKOAuthServerProvider->>SessionRepository: create family access session
  OAuthClient->>SDKOAuthServerProvider: exchange refresh token
  SDKOAuthServerProvider->>RefreshTokenFamilyRepository: consume and rotate token
  RefreshTokenFamilyRepository-->>SDKOAuthServerProvider: rotated or replay result
  SDKOAuthServerProvider->>SessionRepository: create next session or revoke family sessions
  SDKOAuthServerProvider-->>OAuthClient: return token response
Loading

Possibly related PRs

Poem

A bunny hops where tokens turn,
One-use secrets safely burn.
Families guard each leafy trail,
Locks keep racing paws from fail.
Loopback paths now bloom bright—
Fresh access by moonlit night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: rotating OAuth refresh tokens.
Linked Issues check ✅ Passed The changes implement loopback CSP fixes and refresh-token rotation/error handling required by #403 and #404.
Out of Scope Changes check ✅ Passed The added storage, identity, and test changes support refresh-token rotation and consent redirect handling, with no clear unrelated scope.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issues-oauth-403-404

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (11)
test/e2e/oauth-loopback-consent.e2e.test.ts (2)

235-239: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

waitForCallback never rejects.

If Approve fails to redirect, the promise hangs until the 30 s test timeout, masking the real failure point. A bounded timeout with a descriptive rejection makes diagnosis faster.

♻️ Add a bounded wait
-function waitForCallback(): Promise<URL> {
-  return new Promise<URL>((resolve) => {
-    resolveCallback = resolve;
-  });
-}
+function waitForCallback(timeoutMs = 10_000): Promise<URL> {
+  return new Promise<URL>((resolve, reject) => {
+    const timer = setTimeout(() => {
+      resolveCallback = undefined;
+      reject(new Error('Timed out waiting for the loopback callback'));
+    }, timeoutMs);
+    resolveCallback = (url) => {
+      clearTimeout(timer);
+      resolve(url);
+    };
+  });
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/oauth-loopback-consent.e2e.test.ts` around lines 235 - 239, Update
waitForCallback to reject with a descriptive error after a bounded timeout when
the callback is not received, while preserving resolveCallback for successful
redirects and clearing the timeout once it resolves.

78-232: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider splitting this into per-phase tests.

One ~150-line it covers DCR, consent CSP, PKCE exchange, scope/resource rejection, rotation, replay, and revocation. A failure anywhere aborts the rest and gives a vague signal. Sequential its sharing state via the describe scope (or it.sequential) would localize failures without re-running the browser flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/oauth-loopback-consent.e2e.test.ts` around lines 78 - 232, Split the
monolithic test around its existing phases into sequential per-phase tests,
using shared describe-scope state for the client, tokens, and authorization
results so the browser consent flow is not repeated. Keep dedicated assertions
for DCR/consent CSP and PKCE exchange, scope/resource rejection, refresh
rotation and replay, and revocation; ensure setup dependencies run in order and
retain the shared context cleanup.
test/e2e/fixtures/oauth-refresh-worker.mjs (1)

18-26: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the sleep buffer out of the wait loop.

A fresh SharedArrayBuffer/Int32Array per 10 ms tick is needless churn; allocate once before the loop. Otherwise the blocking-sleep approach is the right call inside this synchronous callback.

♻️ Allocate the wait buffer once
     if (releasePath) {
       const deadline = Date.now() + 10_000;
+      const sleepHandle = new Int32Array(new SharedArrayBuffer(4));
       while (!fs.existsSync(releasePath) && Date.now() < deadline) {
-        Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
+        Atomics.wait(sleepHandle, 0, 0, 10);
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/fixtures/oauth-refresh-worker.mjs` around lines 18 - 26, Update the
releasePath wait loop to allocate the SharedArrayBuffer and Int32Array sleep
buffer once before entering the loop, then reuse that buffer for each
Atomics.wait call while preserving the existing timeout and release-file checks.
src/auth/sdkOAuthServerProvider.refresh.test.ts (2)

326-330: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the helper for what it does.

readOnlyFamily reads as "read-only family"; readSoleFamily (or readTheOnlyFamily) makes the single-family assertion intent obvious. Also consider a typed return instead of Record<string, any>.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/sdkOAuthServerProvider.refresh.test.ts` around lines 326 - 330,
Rename the readOnlyFamily helper to readSoleFamily (or readTheOnlyFamily) so its
single-file assertion is explicit, and update all call sites accordingly.
Replace the broad Record<string, any> return type with the appropriate concrete
type already used for parsed family data, if available.

111-127: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

mockImplementationOnce + mockRestore is redundant.

mockImplementationOnce already falls back to the original after the first call, so the follow-up assertion at Line 124 would pass even without the restore. Prefer mockImplementation so the second exchange genuinely proves the spy was removed, or drop the mockRestore.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/sdkOAuthServerProvider.refresh.test.ts` around lines 111 - 127,
Simplify the spy lifecycle in the test around createSession: either use
persistent mockImplementation and explicitly mockRestore before the second
exchange, or remove the restore and rely on mockImplementationOnce. Ensure the
follow-up exchange genuinely verifies the intended post-failure behavior without
redundant setup.
test/e2e/oauth-refresh-concurrency.e2e.test.ts (2)

34-51: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard provider shutdown with try/finally.

If authCodeRepository.create or exchangeAuthorizationCode throws, provider.shutdown() at Line 51 is skipped and storage handles/locks leak into the rest of the suite, while afterEach still deletes the directory underneath them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/oauth-refresh-concurrency.e2e.test.ts` around lines 34 - 51, Wrap
the provider setup and authorization-code exchange flow in a try/finally block
so provider.shutdown() always executes, including when authCodeRepository.create
or exchangeAuthorizationCode throws. Keep the existing setup behavior unchanged
while ensuring cleanup occurs before afterEach removes the temporary directory.

27-31: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No child-process cleanup on failure.

runWorker never exposes the child, so if first.result rejects (or the test fails mid-flight) the spawned workers keep running while afterEach deletes tempDir beneath them, producing confusing secondary errors and leaked processes.

♻️ Track and kill children in teardown
 describe('refresh token family cross-process persistence', () => {
   let tempDir: string | undefined;
+  const children: ReturnType<typeof spawn>[] = [];
 
   afterEach(() => {
+    for (const child of children.splice(0)) {
+      if (child.exitCode === null) {
+        child.kill('SIGKILL');
+      }
+    }
     if (tempDir) {
       fs.rmSync(tempDir, { recursive: true, force: true });
     }
   });
-): { result: Promise<{ status: string }> } {
+  children: ReturnType<typeof spawn>[],
+): { result: Promise<{ status: string }> } {
   const child = spawn(
     process.execPath,
     [
@@
     { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] },
   );
+  children.push(child);

Also applies to: 69-112

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/oauth-refresh-concurrency.e2e.test.ts` around lines 27 - 31, Update
the runWorker test helper and teardown to retain references to every spawned
child process and kill them during afterEach before removing tempDir. Ensure
cleanup runs even when first.result rejects or the test fails mid-flight, while
preserving the existing worker execution behavior.
docs/adr/0011-oauth-refresh-tokens-use-rotating-families.md (1)

1-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align this ADR with the section format.

docs/adr/0010 already uses a small frontmatter header (status: accepted), but this ADR has only the title and content. Add the ADR’s status/frontmatter so it matches the established ADR layout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/0011-oauth-refresh-tokens-use-rotating-families.md` around lines 1 -
8, Update the frontmatter at the beginning of the ADR so it includes the
established accepted status used by docs/adr/0010, placing it before the title
while preserving the existing title and content unchanged.

Source: Coding guidelines

src/auth/storage/refreshTokenFamilyRepository.ts (1)

108-113: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated token-state derivation.

This inline ternary re-implements getTokenState (lines 77-88) with a slightly different lookup source (located.lookup vs a fresh readLookup). Extracting a shared private helper that accepts an optional pre-read lookup would keep the two paths from drifting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/storage/refreshTokenFamilyRepository.ts` around lines 108 - 113,
Update the token-state logic in the repository method containing the inline
ternary to reuse a shared private helper, rather than duplicating
getTokenState’s conditions. Make the helper accept an optional pre-read lookup
so it can use located.lookup when available while preserving the fresh
readLookup behavior in getTokenState, and ensure both paths derive identical
states.
src/auth/storage/fileStorageService.ts (1)

407-411: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

releaseLock throwing from the finally block can mask the operation's outcome.

If the release rename fails, the thrown error replaces the operation's return value or its original error. Log and swallow instead — abandoned lock directories are already reclaimable via reclaimAbandonedLock.

♻️ Proposed change
     } catch (error) {
       logger.error(`Failed to release storage lock ${lockPath}: ${error}`);
-      throw error;
     }

Also applies to: 620-636

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/storage/fileStorageService.ts` around lines 407 - 411, Update the
lock-release cleanup around the operation wrapper and the corresponding flow
near the second release site so errors from releaseLock do not replace the
operation’s return value or original exception. Catch release failures, log
them, and swallow them while preserving the existing reclaimAbandonedLock
behavior for abandoned lock directories.
src/auth/sessionTypes.ts (1)

40-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the top-level Zod UUID schema.

This project uses Zod 4, where z.string().uuid() is deprecated; use z.uuid() here instead.

♻️ Proposed change
-  accessTokenIds: z.array(z.string().uuid()),
+  accessTokenIds: z.array(z.uuid()),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/sessionTypes.ts` at line 40, Update the accessTokenIds schema to use
Zod 4’s top-level z.uuid() validator instead of the deprecated z.string().uuid()
chain, while preserving the array-of-UUIDs validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/auth/oauthAuthorizationFlow.ts`:
- Around line 18-25: The widened createSessionWithId contract is not propagated
by createOAuthAuthorizationFlowFromStorage. Update its createSessionWithId
adapter and the corresponding interface declaration to accept optional
refreshFamilyId and pass it through to sessionRepository.createWithId;
alternatively remove the optional parameter consistently from both interfaces
until supported by callers.

In `@src/auth/sdkOAuthServerProvider.ts`:
- Around line 450-466: Remove the unsynchronized findByToken/getTokenState
validation and the tokenState branches from the refresh-token flow around the
repository calls. Invoke repository.consume as the single authoritative,
lock-held decision point, then preserve the existing rotation.status ===
'replay' handling to revoke family access tokens and return the replay error;
continue the normal rotation path for successful results.

In `@src/auth/storage/fileStorageService.ts`:
- Around line 533-552: Broaden the Windows-specific tolerated error-code
allowlist in flushStorageDirectory so directory fsync failures from both
fs.openSync and fs.fsyncSync do not propagate for supported cases. Extend the
existing ['EINVAL', 'ENOTSUP', 'EPERM'] set to include EISDIR and EACCES, while
preserving rethrow behavior for other errors and non-Windows platforms.

In `@test/e2e/oauth-refresh-concurrency.e2e.test.ts`:
- Around line 56-65: Replace the fixed delay before the secondMarker assertion
with an explicit startup synchronization. Update the runWorker fixture or worker
entrypoint to emit a distinct started signal before consume, await that signal
for the second worker, then assert secondMarker is absent before releasing
releasePath. Preserve the existing Promise.all result assertion for the replay
and rotated outcomes.

---

Nitpick comments:
In `@docs/adr/0011-oauth-refresh-tokens-use-rotating-families.md`:
- Around line 1-8: Update the frontmatter at the beginning of the ADR so it
includes the established accepted status used by docs/adr/0010, placing it
before the title while preserving the existing title and content unchanged.

In `@src/auth/sdkOAuthServerProvider.refresh.test.ts`:
- Around line 326-330: Rename the readOnlyFamily helper to readSoleFamily (or
readTheOnlyFamily) so its single-file assertion is explicit, and update all call
sites accordingly. Replace the broad Record<string, any> return type with the
appropriate concrete type already used for parsed family data, if available.
- Around line 111-127: Simplify the spy lifecycle in the test around
createSession: either use persistent mockImplementation and explicitly
mockRestore before the second exchange, or remove the restore and rely on
mockImplementationOnce. Ensure the follow-up exchange genuinely verifies the
intended post-failure behavior without redundant setup.

In `@src/auth/sessionTypes.ts`:
- Line 40: Update the accessTokenIds schema to use Zod 4’s top-level z.uuid()
validator instead of the deprecated z.string().uuid() chain, while preserving
the array-of-UUIDs validation.

In `@src/auth/storage/fileStorageService.ts`:
- Around line 407-411: Update the lock-release cleanup around the operation
wrapper and the corresponding flow near the second release site so errors from
releaseLock do not replace the operation’s return value or original exception.
Catch release failures, log them, and swallow them while preserving the existing
reclaimAbandonedLock behavior for abandoned lock directories.

In `@src/auth/storage/refreshTokenFamilyRepository.ts`:
- Around line 108-113: Update the token-state logic in the repository method
containing the inline ternary to reuse a shared private helper, rather than
duplicating getTokenState’s conditions. Make the helper accept an optional
pre-read lookup so it can use located.lookup when available while preserving the
fresh readLookup behavior in getTokenState, and ensure both paths derive
identical states.

In `@test/e2e/fixtures/oauth-refresh-worker.mjs`:
- Around line 18-26: Update the releasePath wait loop to allocate the
SharedArrayBuffer and Int32Array sleep buffer once before entering the loop,
then reuse that buffer for each Atomics.wait call while preserving the existing
timeout and release-file checks.

In `@test/e2e/oauth-loopback-consent.e2e.test.ts`:
- Around line 235-239: Update waitForCallback to reject with a descriptive error
after a bounded timeout when the callback is not received, while preserving
resolveCallback for successful redirects and clearing the timeout once it
resolves.
- Around line 78-232: Split the monolithic test around its existing phases into
sequential per-phase tests, using shared describe-scope state for the client,
tokens, and authorization results so the browser consent flow is not repeated.
Keep dedicated assertions for DCR/consent CSP and PKCE exchange, scope/resource
rejection, refresh rotation and replay, and revocation; ensure setup
dependencies run in order and retain the shared context cleanup.

In `@test/e2e/oauth-refresh-concurrency.e2e.test.ts`:
- Around line 34-51: Wrap the provider setup and authorization-code exchange
flow in a try/finally block so provider.shutdown() always executes, including
when authCodeRepository.create or exchangeAuthorizationCode throws. Keep the
existing setup behavior unchanged while ensuring cleanup occurs before afterEach
removes the temporary directory.
- Around line 27-31: Update the runWorker test helper and teardown to retain
references to every spawned child process and kill them during afterEach before
removing tempDir. Ensure cleanup runs even when first.result rejects or the test
fails mid-flight, while preserving the existing worker execution behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f10ce65-0d16-4b1a-b12d-799d2f2a895b

📥 Commits

Reviewing files that changed from the base of the PR and between d31877a and cace717.

📒 Files selected for processing (19)
  • docs/adr/0011-oauth-refresh-tokens-use-rotating-families.md
  • docs/en/guide/advanced/authentication.md
  • docs/zh/guide/advanced/authentication.md
  • src/auth/oauthAuthorizationFlow.ts
  • src/auth/sdkOAuthServerProvider.refresh.test.ts
  • src/auth/sdkOAuthServerProvider.test.ts
  • src/auth/sdkOAuthServerProvider.ts
  • src/auth/sessionTypes.ts
  • src/auth/storage/fileStorageService.test.ts
  • src/auth/storage/fileStorageService.ts
  • src/auth/storage/oauthStorageService.ts
  • src/auth/storage/refreshTokenFamilyRepository.ts
  • src/auth/storage/sessionRepository.ts
  • src/constants/auth.ts
  • src/core/runtime/runtimeIdentityService.ts
  • src/transport/http/server.ts
  • test/e2e/fixtures/oauth-refresh-worker.mjs
  • test/e2e/oauth-loopback-consent.e2e.test.ts
  • test/e2e/oauth-refresh-concurrency.e2e.test.ts

Comment thread src/auth/oauthAuthorizationFlow.ts
Comment thread src/auth/sdkOAuthServerProvider.ts Outdated
Comment thread src/auth/storage/fileStorageService.ts
Comment thread test/e2e/oauth-refresh-concurrency.e2e.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/auth/storage/fileStorageService.ts (1)

621-635: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not report success while leaving an unreleasable live-process lock.

If the rename at Line 630 fails, .refresh-family.lock remains owned by this still-live PID. reclaimAbandonedLock() will not reclaim it, so subsequent refresh rotations time out for the process lifetime. Retry/fallback-delete the verified lock directory and surface unrecoverable cleanup failures; extend the test to acquire the lock again after the injected failure.

  • src/auth/storage/fileStorageService.ts#L621-L635: recover the verified lock directory before returning, or propagate an unrecoverable release failure.
  • src/auth/storage/fileStorageService.test.ts#L146-L155: verify a subsequent withExclusiveLock call succeeds after simulated cleanup failure.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/storage/fileStorageService.ts` around lines 621 - 635, Update
fileStorageService.ts lines 621-635 in releaseLock to retry or fallback-delete
the verified lock directory when fs.renameSync fails, and surface any
unrecoverable cleanup failure instead of silently returning with a live lock.
Extend fileStorageService.test.ts lines 146-155 to inject the cleanup failure
and verify a subsequent withExclusiveLock call succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/auth/storage/fileStorageService.ts`:
- Around line 621-635: Update fileStorageService.ts lines 621-635 in releaseLock
to retry or fallback-delete the verified lock directory when fs.renameSync
fails, and surface any unrecoverable cleanup failure instead of silently
returning with a live lock. Extend fileStorageService.test.ts lines 146-155 to
inject the cleanup failure and verify a subsequent withExclusiveLock call
succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 86121923-e0bf-469d-a80a-79243c2462e5

📥 Commits

Reviewing files that changed from the base of the PR and between cace717 and 48014e9.

📒 Files selected for processing (11)
  • docs/adr/0011-oauth-refresh-tokens-use-rotating-families.md
  • src/auth/oauthAuthorizationFlow.ts
  • src/auth/sdkOAuthServerProvider.refresh.test.ts
  • src/auth/sdkOAuthServerProvider.ts
  • src/auth/sessionTypes.ts
  • src/auth/storage/fileStorageService.test.ts
  • src/auth/storage/fileStorageService.ts
  • src/auth/storage/refreshTokenFamilyRepository.ts
  • test/e2e/fixtures/oauth-refresh-worker.mjs
  • test/e2e/oauth-loopback-consent.e2e.test.ts
  • test/e2e/oauth-refresh-concurrency.e2e.test.ts
💤 Files with no reviewable changes (1)
  • src/auth/sdkOAuthServerProvider.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/auth/storage/fileStorageService.ts (2)

334-348: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Require a schema for every persisted-data read.

Making schema optional preserves an unvalidated JSON.parse/cast path; malformed records can bypass expiration/type checks. Make the schema required and update callers to supply their record schema.

Proposed direction
-readData<T extends ExpirableData>(filePrefix: string, id: string, schema?: ZodType<T>): T | null {
+readData<T extends ExpirableData>(filePrefix: string, id: string, schema: ZodType<T>): T | null {
 ...
-  const parsedData = schema ? schema.parse(parsed) : (parsed as T);
+  const parsedData = schema.parse(parsed);

As per coding guidelines, “Use Zod schemas for all input validation and API boundaries.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/storage/fileStorageService.ts` around lines 334 - 348, Update
readData in FileStorageService to require a ZodType<T> schema instead of
accepting an optional schema, and always validate parsed JSON through that
schema before returning it. Update every readData caller to pass the appropriate
record schema, removing the unchecked JSON.parse cast path while preserving
existing expiration and error handling.

Source: Coding guidelines


555-580: 🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Prevent owner-less lock reclamation from granting two owners.

After mkdirSync at Line 557, the acquiring process can be paused for over one second before writing owner.json. Another process then tombstones the owner-less directory and acquires a new lock. The first process can resume and overwrite the new owner file at Line 567, so both operations proceed concurrently.

Create owner.json exclusively (flag: 'wx'), treat an existing/different owner as acquisition failure, and only remove a lock directory after re-reading and confirming this operation owns it. Add a deterministic regression test for this interleaving.

Also applies to: 584-618

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/storage/fileStorageService.ts` around lines 555 - 580, Update
tryAcquireLock and its cleanup path so owner.json is created exclusively with
flag 'wx'; treat an existing owner, including one written after lock
reclamation, as acquisition failure rather than overwriting it. Before removing
the lock directory after initialization failure, re-read owner.json and remove
the directory only when it still identifies the current operation; otherwise
preserve the newer owner and return failure. Add a deterministic regression test
covering the pause after mkdirSync, reclamation by another process, and
prevention of concurrent ownership.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/auth/storage/fileStorageService.test.ts`:
- Around line 146-158: Update the test around service.withExclusiveLock to
assert that failedReleaseRename is true after the first lock release, confirming
the injected .releasing rename failure actually occurred while preserving the
existing recovery and reacquisition assertions.

---

Outside diff comments:
In `@src/auth/storage/fileStorageService.ts`:
- Around line 334-348: Update readData in FileStorageService to require a
ZodType<T> schema instead of accepting an optional schema, and always validate
parsed JSON through that schema before returning it. Update every readData
caller to pass the appropriate record schema, removing the unchecked JSON.parse
cast path while preserving existing expiration and error handling.
- Around line 555-580: Update tryAcquireLock and its cleanup path so owner.json
is created exclusively with flag 'wx'; treat an existing owner, including one
written after lock reclamation, as acquisition failure rather than overwriting
it. Before removing the lock directory after initialization failure, re-read
owner.json and remove the directory only when it still identifies the current
operation; otherwise preserve the newer owner and return failure. Add a
deterministic regression test covering the pause after mkdirSync, reclamation by
another process, and prevention of concurrent ownership.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 40a2c584-d991-4f13-a127-29bd9ce562b4

📥 Commits

Reviewing files that changed from the base of the PR and between 48014e9 and 1c55765.

📒 Files selected for processing (2)
  • src/auth/storage/fileStorageService.test.ts
  • src/auth/storage/fileStorageService.ts

Comment thread src/auth/storage/fileStorageService.test.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant