Skip to content

fix: resolve 16 deep-audit findings (data-loss, rotation, redaction, concurrency) - #165

Merged
ndycode merged 2 commits into
mainfrom
fix/deep-audit-findings
Jun 6, 2026
Merged

ndycode merged 2 commits into
mainfrom
fix/deep-audit-findings

Conversation

@ndycode

@ndycode ndycode commented Jun 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

A deep multi-agent audit of the whole codebase (post #164) surfaced 21 candidate issues; 16 distinct real bugs survived adversarial verification and are fixed here. Each fix has a mutation-verified regression test (it fails against the old behavior, passes against the fix).

Findings fixed

Data-loss / credentials (high)

  • Transient refresh failures no longer cause permanent removal. Network blips and upstream 5xx during token refresh were counted toward MAX_AUTH_FAILURES_BEFORE_REMOVAL, so an outage could delete valid credentials. CodexAuthError now carries the refresh reason/statusCode; the catch only counts genuine auth invalidation (4xx / missing_refresh) toward removal, and cools down + rotates on transient failures. (lib/errors.ts, lib/request/fetch-helpers.ts, index.ts)
  • Keychain load no longer swallows forward-compat errors. The keychain branch caught the UNSUPPORTED_SCHEMA_VERSION / unknown-V2 StorageError that normalizeAccountStorage throws, silently falling through and risking a clobber on next save. These now propagate, matching the JSON path. (lib/storage/load-save.ts)
  • Workspace-deactivation removes only the deactivated workspace. It called removeAccountsWithSameRefreshToken, dropping still-valid sibling workspaces that share one refresh token. Now scoped by workspace identity via removeAccountsByWorkspaceIdentity. (lib/accounts/recovery.ts, index.ts)
  • Refresh-token rotation propagates to siblings. A rotation in the hot path updated only one account; org-variant siblings sharing the pre-rotation token were orphaned and eventually removed. updateFromAuth now propagates the new token to siblings. (lib/accounts/state.ts)
  • Account-check / email hydration writes are transactional. runAccountCheck and hydrateEmails did load → long-network-loop → save against a stale snapshot, clobbering concurrent saves. Both now persist via withAccountStorageTransaction, re-applying only the token/identity fields they refresh. (index.ts)

Rotation correctness

  • Heuristic trackers remap on removal. Health-score, token-bucket, and rate-limit-backoff state is keyed by mutable account.index; removeAccount reindexed survivors but left tracker keys stale, so an account could inherit a removed/shifted neighbor's drained bucket or rate-limited score. Added remapAfterRemoval / remapIndexedKeys wired into removeAccount. (lib/rotation.ts, lib/request/rate-limit-backoff.ts, lib/accounts/state.ts)
  • Local token-bucket depletion rotates instead of aborting. A depleted bucket break'd the whole rotation loop (503), abandoning healthy accounts — now continues to the next account. (index.ts)
  • Disabled missing-scope OAuth fallback no longer pushes a duplicate index: 0. (lib/accounts/state.ts)

Logic / retry

  • Empty-response retry actually retries. It break'd to rotation, a no-op for single-account pools that surfaced a misleading 503; now re-issues against the same account. (index.ts)
  • retry_after_ms vs retry_after scaling. The two were collapsed and run through a <1000 "looks like seconds" heuristic, ballooning a sub-second retry_after_ms into minutes. Now scaled distinctly. (lib/request/fetch-helpers.ts)

Resource leak / resilience

  • Stream-stall / SSE-parse exceptions from handleSuccessResponse escaped the loop with the consumed token un-refunded and no rotation. Now wrapped: refund + record failure + rotate. (index.ts)
  • invalidateAccountManagerCache flushes the pending debounced save and disposes the outgoing manager's shutdown handler/timer. (index.ts)

Security / privacy

  • codex-diff redaction is key-aware. Opaque refresh tokens / account ids / labels are not token-shaped, so the shape-only maskString emitted them verbatim. Now masked by field name. (lib/tools/codex-diff.ts)
  • Flagged storage uses the keychain when CODEX_KEYCHAIN=1 instead of writing plaintext refresh tokens to disk. (lib/storage/flagged.ts, lib/storage/keychain.ts)
  • Logger: emails masked domain-preserving (not the token mask that leaked the first 6 chars); cookie-family headers added to the sensitive-key set. (lib/logger.ts)

Verification

  • npm run typecheck: clean
  • npm run lint: clean
  • npm test: 92 files, 2472 passed, 1 skipped (+31 regression tests)
  • npm run build: succeeds
  • Every new regression test was mutation-verified — reverting its fix makes the test fail.

Notes

  • 4 candidate findings were rejected in verification (reason-weighted-backoff "unused", expires_in edge, tools confirm-guard, backup pruning) and are intentionally not addressed here.
  • No version bump; release handled separately.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added secure keychain storage support for account credentials.
  • Bug Fixes

    • Improved authentication token refresh handling with better transient failure detection and recovery.
    • Fixed workspace deactivation to remove only the targeted workspace while preserving sibling workspaces that share credentials.
    • Enhanced error recovery for rate-limit and token bucket scenarios during request execution.
    • Better handling of response parsing failures with automatic token refund and circuit breaker updates.
  • Chores

    • Enhanced security logging with improved redaction of sensitive data (emails, cookies, tokens).

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr addresses 16 verified bugs across data-loss, rotation correctness, concurrency, and security — all with mutation-tested regression tests. the fixes are generally well-scoped and the concurrency approach (storage transactions, key-based merges) is sound.

  • data-loss / credentials: transient 5xx/network refresh failures now cool down instead of counting toward removal; keychain forward-compat errors propagate correctly; workspace deactivation removes only the targeted workspace; refresh-token rotation propagates to org siblings; hydration and health-check writes are transactional.
  • rotation / retry: tracker indices remap on account removal (HealthScoreTracker, TokenBucketTracker, rate-limit backoff); depleted token-bucket now continues to next account; empty-response handler correctly retries same account; retry_after_ms vs retry_after unit scaling fixed.
  • security / privacy: codex-diff gains key-aware redaction for token-shaped fields; flagged credentials use keychain when CODEX_KEYCHAIN=1; logger adds domain-preserving email mask and cookie-family headers — however accountid / email are absent from SENSITIVE_LEAF_KEYS in codex-diff.ts, leaving account IDs verbatim in diff output despite the pr description claiming they are masked.

Confidence Score: 3/5

safe to merge the auth/rotation/concurrency fixes, but the codex-diff redaction gap means account IDs still appear verbatim in diff output — the opposite of what the pr claims to fix

the auth, rotation, and storage-transaction fixes are well-reasoned and mutation-tested. the concrete gap is in lib/tools/codex-diff.ts: SENSITIVE_LEAF_KEYS does not include accountid or email, so those fields emit verbatim in diff output — the pr description explicitly says this was fixed. the concurrency fix for hydrationUpdates keyed by refresh token has a narrow rotation-miss race that degrades silently in an unlikely concurrent scenario

lib/tools/codex-diff.ts — SENSITIVE_LEAF_KEYS needs accountid and email to match what the logger and pr description guarantee. index.ts hydration transaction (keying by workspace identity instead of refresh token) is worth a follow-up

Security Review

  • lib/tools/codex-diff.ts — account ID / email leak in diff output: SENSITIVE_LEAF_KEYS does not include accountid, account_id, or email, so computeCodexDiff emits those values verbatim. the logger and the pr description both treat accountid as sensitive; the codex-diff implementation does not. opaque account ids (e.g. user_xxxx) are not jwt-shaped and are not caught by maskString.

Important Files Changed

Filename Overview
lib/tools/codex-diff.ts key-aware redaction layer is a good pattern, but SENSITIVE_LEAF_KEYS omits accountid, email, and clientid that the logger masks — account IDs still emit verbatim in diff output, contradicting the stated fix
index.ts large set of targeted fixes: transient-refresh error branching, token-bucket break→continue, workspace-scoped removal, stream-error catch, withAccountStorageTransaction for hydration and runAccountCheck; hydrationUpdates keyed by refresh token has narrow rotation-miss race
lib/accounts/state.ts sibling refresh-token propagation and tracker remap on removal look correct; duplicate authFailuresByRefreshToken.delete in propagateRotatedRefreshTokenToSiblings is harmless but confusing
lib/request/fetch-helpers.ts retry_after_ms / retry_after scaling fix is correct and well-tested; isTransient classification is reasonable; normalizeRetryAfter removed cleanly
lib/accounts/recovery.ts removeAccountsByWorkspaceIdentity correctly scopes removal to the deactivated workspace; auth-failure counter cleared only when last sibling with that token is gone
lib/rotation.ts remapIndexedKeys and remapAfterRemoval on HealthScoreTracker and TokenBucketTracker correctly shift surviving account indices after removal
lib/storage/load-save.ts forward-compat StorageError now correctly propagates from keychain path; parse vs. normalization errors split cleanly
lib/storage/flagged.ts keychain-first load/save for flagged accounts correct; windows EPERM on unlink handled gracefully

Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
lib/tools/codex-diff.ts:131-145
**`accountid` absent from `SENSITIVE_LEAF_KEYS`**

the pr description explicitly says "opaque refresh tokens / account ids / labels are not token-shaped, so the shape-only `maskString` emitted them verbatim. Now masked by field name." — but `accountid` (and `account_id`) are not in `SENSITIVE_LEAF_KEYS`. the logger's `SENSITIVE_KEYS` set at `lib/logger.ts:83` includes `"accountid"` precisely because account ids are opaque and not caught by `maskString`. `computeCodexDiff` will still emit `accountId` values verbatim, contradicting the stated fix. `email` and `clientid` are also in the logger set but absent here.

### Issue 2 of 3
lib/accounts/state.ts:462-478
`authFailuresByRefreshToken.delete(previousRefreshToken)` is called twice: once in `updateFromAuth` at the top of the `if (previousRefreshToken !== account.refreshToken)` block (line 432), and again here at the end of this private method. the second call is always a no-op since the key was already removed. drop the duplicate or document why it's intentional.

```suggestion
	private propagateRotatedRefreshTokenToSiblings(
		account: ManagedAccount,
		previousRefreshToken: string,
		newRefreshToken: string,
	): void {
		if (!previousRefreshToken || previousRefreshToken === newRefreshToken) return;
		for (const sibling of this.accounts) {
			if (sibling === account) continue;
			if (sibling.refreshToken === previousRefreshToken) {
				sibling.refreshToken = newRefreshToken;
				// The expired access token on the sibling forces a fresh refresh
				// (with the now-valid token) the next time it is selected.
				sibling.expires = 0;
			}
		}
		// Note: caller (updateFromAuth) already deleted this key before invoking
		// this method. The delete here is a no-op but kept for self-containedness.
	}
```

### Issue 3 of 3
index.ts:841-845
**`hydrationUpdates` refresh-token key doesn't survive sibling rotation**

the map is keyed by `originalRefreshToken` (captured before the network call). inside the transaction, accounts are looked up via `hydrationUpdates.get(acc.refreshToken)`. if, during the multi-second hydration network loop, a concurrent request triggers `updateFromAuth` on a sibling which calls `propagateRotatedRefreshTokenToSiblings` — rotating the shared token R0→R1 — the fresh snapshot's `acc.refreshToken` becomes R1 while the map key is still R0. the lookup silently misses and the hydrated email/accountId is dropped for that account. keying `hydrationUpdates` by `getWorkspaceIdentityKey(account)` instead of `originalRefreshToken` would survive token rotation, since workspace identity (org+account id) is stable.

Reviews (1): Last reviewed commit: "fix: complete remaining audit findings a..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

ndycode and others added 2 commits June 6, 2026 07:40
Deep multi-agent audit of the codebase surfaced real runtime bugs. This
commit fixes the contained set (4 more complex ones follow):

data-loss / credentials:
- Transient refresh failures (network/5xx) no longer count toward permanent
  account removal — they now cool down and rotate. CodexAuthError carries the
  refresh failure reason/statusCode so the catch can classify (errors.ts,
  request/fetch-helpers.ts, index.ts).
- Keychain load now lets normalizeAccountStorage's StorageError
  (UNSUPPORTED_SCHEMA_VERSION / unknown-V2) propagate instead of silently
  falling through, restoring the forward-compat guard (storage/load-save.ts).
- Workspace-deactivation removes ONLY the deactivated workspace by identity,
  not every refresh-token sibling, so still-valid workspaces survive
  (accounts/recovery.ts removeAccountsByWorkspaceIdentity, index.ts).

rotation correctness:
- Health/token-bucket/rate-limit-backoff trackers are keyed by mutable
  account.index; removeAccount now remaps that state to follow the reindex so
  survivors don't inherit a removed/shifted account's heuristics (rotation.ts,
  request/rate-limit-backoff.ts, accounts/state.ts).
- Local token-bucket depletion now rotates to the next account (continue)
  instead of aborting the whole pool with a 503 (index.ts).
- Disabled missing-scope OAuth fallback no longer pushes a duplicate index:0
  (accounts/state.ts).

logic / retry:
- Empty-response retry re-issues against the same account (continue) instead
  of a no-op rotation that 503'd single-account pools (index.ts).
- retry_after_ms (ms) and retry_after (seconds) are scaled distinctly; a
  sub-second retry_after_ms no longer balloons into minutes
  (request/fetch-helpers.ts).

security / privacy:
- codex-diff redaction is now key-aware: opaque refresh tokens / account ids /
  labels are masked by field name, not only by token shape (tools/codex-diff.ts).
- logger sanitizeValue masks email keys with maskEmail (domain-preserving)
  rather than maskToken; cookie-family headers added to SENSITIVE_KEYS (logger.ts).

resource leak:
- invalidateAccountManagerCache flushes the pending debounced save and disposes
  the outgoing manager's shutdown handler/timer (index.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the remaining 4 audit findings and adds regression tests across the set:

- Refresh-token rotation in the hot path now propagates the new token to
  sibling org-variant accounts that shared the pre-refresh token, so a rotation
  no longer orphans (and eventually removes) still-valid workspaces
  (accounts/state.ts updateFromAuth -> propagateRotatedRefreshTokenToSiblings).
- hydrateEmails and runAccountCheck persist via withAccountStorageTransaction
  against a fresh snapshot, re-applying only the token/identity fields they
  refresh — closing the load->network-loop->save lost-update race (index.ts).
- handleSuccessResponse is wrapped in try/catch: a stream stall / SSE-parse
  failure now refunds the consumed token, records the breaker/account failure,
  and rotates instead of escaping the loop with a leaked token (index.ts).
- Flagged-account storage routes through the OS keychain when CODEX_KEYCHAIN=1
  (load + save, JSON fallback), so flagged refresh tokens are no longer written
  as plaintext; verify-flagged still has the token it needs to restore
  (storage/flagged.ts, storage/keychain.ts buildKeychainFlaggedKey + helpers).

Regression tests (mutation-verified to fail against the old behavior):
- test/rotation-remap.test.ts, test/accounts-remove-remap.test.ts — tracker
  index remap on removal (health/token-bucket/backoff).
- test/accounts-workspace-removal.test.ts — workspace-identity scoped removal.
- test/accounts-refresh-propagation.test.ts — sibling refresh-token propagation.
- test/fetch-helpers.test.ts — retry_after_ms (ms) vs retry_after (s) scaling.
- test/tools-codex-diff.test.ts — key-aware redaction of opaque tokens.
- test/logger.test.ts — email domain-preserving mask + cookie-header masking.

Full suite: 92 files, 2472 passed, 1 skipped. Typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0fc52c4a-efef-4c49-8c3b-6002dd1d594e

📥 Commits

Reviewing files that changed from the base of the PR and between 2094bf0 and 8d000c5.

📒 Files selected for processing (21)
  • index.ts
  • lib/accounts.ts
  • lib/accounts/recovery.ts
  • lib/accounts/state.ts
  • lib/errors.ts
  • lib/logger.ts
  • lib/request/fetch-helpers.ts
  • lib/request/rate-limit-backoff.ts
  • lib/rotation.ts
  • lib/storage/flagged.ts
  • lib/storage/keychain.ts
  • lib/storage/load-save.ts
  • lib/tools/codex-diff.ts
  • test/accounts-refresh-propagation.test.ts
  • test/accounts-remove-remap.test.ts
  • test/accounts-workspace-removal.test.ts
  • test/fetch-helpers.test.ts
  • test/index.test.ts
  • test/logger.test.ts
  • test/rotation-remap.test.ts
  • test/tools-codex-diff.test.ts

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting


📝 Walkthrough

Walkthrough

This PR consolidates seven audit fixes and operational improvements across account management, auth error handling, request/response robustness, and credential storage. It refactors workspace deactivation to remove only the targeted workspace (preserving refresh-token siblings), propagates refresh-token rotations to sibling accounts, implements index-remapping infrastructure for tracker state after removals, upgrades auth-error classification to distinguish transient from permanent refresh failures, adds OS keychain storage for flagged credentials, improves request-flow error handling with stream parsing recovery and token refunds, and enhances redaction for sensitive fields in logs and diffs.

Changes

Account Management and Auth Refresh

Layer / File(s) Summary
Retryable vs non-retryable refresh failure classification
lib/request/fetch-helpers.ts, lib/errors.ts, test/fetch-helpers.test.ts
Token-refresh failures are now classified as retryable (network/invalid response/5xx) or non-retryable based on error reason and HTTP status; rate-limit retry_after_ms (milliseconds) is separated from retry_after (seconds) to avoid scale confusion.
Workspace-identity-scoped account removal
lib/accounts.ts, lib/accounts/recovery.ts, lib/accounts/state.ts, test/accounts-workspace-removal.test.ts, test/index.test.ts
removeAccountsByWorkspaceIdentity removes only accounts matching a workspace identity (org/accountId), preserving refresh-token siblings in other workspaces; auth-failure state is cleared only when the last account using a refresh token is removed.
Refresh-token rotation propagation to siblings
lib/accounts/state.ts, test/accounts-refresh-propagation.test.ts
When updateFromAuth detects a rotated refresh token, all sibling accounts sharing the prior token are updated with the new token and their expires is reset to 0, forcing re-authentication on next selection.
Index remapping infrastructure for tracker state
lib/rotation.ts, lib/request/rate-limit-backoff.ts, lib/accounts/state.ts, test/rotation-remap.test.ts, test/accounts-remove-remap.test.ts
Generic remapIndexedKeys helper and per-tracker remapAfterRemoval methods shift indexed-key map entries when an account is removed; integrates into account-removal path for HealthScoreTracker, TokenBucketTracker, and rate-limit backoff state.

Error Handling and Credential Storage

Layer / File(s) Summary
Auth error metadata and logging redaction
lib/errors.ts, lib/logger.ts, test/logger.test.ts, lib/tools/codex-diff.ts, test/tools-codex-diff.test.ts
Adds optional refreshFailureReason and statusCode metadata to CodexAuthError; expands structured-log redaction to mask cookies and email using domain-preserving masking; upgrades codex-diff redaction to be key-aware, masking sensitive leaf fields like refreshToken by terminal key.
OS keychain storage for flagged credentials
lib/storage/keychain.ts, lib/storage/flagged.ts, lib/storage/load-save.ts
Adds separate "flagged" namespace in OS keychain with buildKeychainFlaggedKey, readFlaggedFromKeychain, and writeFlaggedToKeychain helpers; integrates keychain-backed storage for flagged accounts with on-disk JSON fallback on write failure; improves keychain error handling in loadAccountsInternal.

Request Handling and Storage Persistence

Layer / File(s) Summary
Account hydration and health-check transactional persistence
index.ts
hydrateEmails now records refreshed fields in a map keyed by original refresh token, then applies updates inside withAccountStorageTransaction to avoid lost-update conflicts; runAccountCheck merges health-check results into on-disk state by workspace identity while preserving concurrent edits.
Request-flow error recovery and token management
index.ts
handleSuccessResponse is wrapped in try/catch for stream/SSE parsing failures—on error, the consumed token is refunded, circuit breaker marked, and account rotated; empty-response retries now continue to the same account instead of rotating; token-bucket depletion skips to the next account to allow healthy accounts to be tried.
Cache invalidation and transient-failure cooldown
index.ts
invalidateAccountManagerCache flushes pending saves and disposes shutdown handlers on the previous manager; transient auth-refresh errors (retryable === true) now cool down affected accounts without incrementing removal counters, then rotate instead of removing credentials.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🐰 Sibling tokens now sync with flair,
Workspace walls keep workspaces fair!
Keychain guards credentials tight,
Stream parsing fixed outright. 🔐

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/deep-audit-findings

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 and usage tips.

@ndycode
ndycode merged commit b8cdf6b into main Jun 6, 2026
1 of 2 checks passed
Comment thread lib/tools/codex-diff.ts
Comment on lines +131 to +145
const SENSITIVE_LEAF_KEYS = new Set([
"refreshtoken",
"accesstoken",
"idtoken",
"token",
"refresh",
"access",
"apikey",
"authorization",
"cookie",
"setcookie",
"clientsecret",
"secret",
"password",
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security accountid absent from SENSITIVE_LEAF_KEYS

the pr description explicitly says "opaque refresh tokens / account ids / labels are not token-shaped, so the shape-only maskString emitted them verbatim. Now masked by field name." — but accountid (and account_id) are not in SENSITIVE_LEAF_KEYS. the logger's SENSITIVE_KEYS set at lib/logger.ts:83 includes "accountid" precisely because account ids are opaque and not caught by maskString. computeCodexDiff will still emit accountId values verbatim, contradicting the stated fix. email and clientid are also in the logger set but absent here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/tools/codex-diff.ts
Line: 131-145

Comment:
**`accountid` absent from `SENSITIVE_LEAF_KEYS`**

the pr description explicitly says "opaque refresh tokens / account ids / labels are not token-shaped, so the shape-only `maskString` emitted them verbatim. Now masked by field name." — but `accountid` (and `account_id`) are not in `SENSITIVE_LEAF_KEYS`. the logger's `SENSITIVE_KEYS` set at `lib/logger.ts:83` includes `"accountid"` precisely because account ids are opaque and not caught by `maskString`. `computeCodexDiff` will still emit `accountId` values verbatim, contradicting the stated fix. `email` and `clientid` are also in the logger set but absent here.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment thread index.ts
Comment on lines +841 to +845
await withAccountStorageTransaction(async (current, persist) => {
if (!current) return;
for (const acc of current.accounts) {
const update = hydrationUpdates.get(acc.refreshToken);
if (!update) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 hydrationUpdates refresh-token key doesn't survive sibling rotation

the map is keyed by originalRefreshToken (captured before the network call). inside the transaction, accounts are looked up via hydrationUpdates.get(acc.refreshToken). if, during the multi-second hydration network loop, a concurrent request triggers updateFromAuth on a sibling which calls propagateRotatedRefreshTokenToSiblings — rotating the shared token R0→R1 — the fresh snapshot's acc.refreshToken becomes R1 while the map key is still R0. the lookup silently misses and the hydrated email/accountId is dropped for that account. keying hydrationUpdates by getWorkspaceIdentityKey(account) instead of originalRefreshToken would survive token rotation, since workspace identity (org+account id) is stable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: index.ts
Line: 841-845

Comment:
**`hydrationUpdates` refresh-token key doesn't survive sibling rotation**

the map is keyed by `originalRefreshToken` (captured before the network call). inside the transaction, accounts are looked up via `hydrationUpdates.get(acc.refreshToken)`. if, during the multi-second hydration network loop, a concurrent request triggers `updateFromAuth` on a sibling which calls `propagateRotatedRefreshTokenToSiblings` — rotating the shared token R0→R1 — the fresh snapshot's `acc.refreshToken` becomes R1 while the map key is still R0. the lookup silently misses and the hydrated email/accountId is dropped for that account. keying `hydrationUpdates` by `getWorkspaceIdentityKey(account)` instead of `originalRefreshToken` would survive token rotation, since workspace identity (org+account id) is stable.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

ndycode added a commit that referenced this pull request Jun 6, 2026
Property-based and concurrency stress tests for the subsystems hardened in #165: tracker index-remap on removal, concurrent storage transactions (lost-update), email/secret redaction invariants, and refresh-token rotation propagation. Each mutation-verified. 2487 tests pass.
@ndycode ndycode mentioned this pull request Jun 6, 2026
ndycode added a commit that referenced this pull request Jun 6, 2026
Minor release on top of 6.2.0: email masking across all display surfaces (#164), 16 deep-audit bug fixes (#165), and the deep stress suite (#166). Bumps package.json, .release-please-manifest.json, and .codex-plugin/plugin.json to 6.3.0. 2487 tests pass; build/lint/typecheck clean; publish dry-run verified.
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