Skip to content

Harden storage transactions against path drift#145

Closed
ndycode wants to merge 1 commit intomainfrom
git-clean/05-storage-hardening
Closed

Harden storage transactions against path drift#145
ndycode wants to merge 1 commit intomainfrom
git-clean/05-storage-hardening

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 20, 2026

Summary

Tightens storage transaction handling when the resolved storage path changes mid-transaction.

What changed

  • keep transaction saves pinned to the original storage path
  • fail fast when export runs from an active transaction after path drift
  • add Windows-equivalent path coverage and fresh-reload regressions

Validation

  • npm run lint
  • npm run typecheck
  • npx vitest run test/storage.test.ts --testNamePattern="fails fast when export is called from an active transaction after the storage path changes|allows equivalent Windows-style storage paths during export from an active transaction|reloads fresh storage after a transaction handler throws|persists transaction updates to the original storage path after path drift|reloads fresh storage after a transaction handler returns successfully|reloads fresh storage after a combined transaction handler throws|reloads fresh storage after a combined transaction handler returns successfully"

Supersedes part of #139.

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 hardens storage transaction semantics against mid-transaction path drift by pinning loadAccountsInternal and saveAccountsUnlocked to the path captured at transaction start, fast-failing exportAccounts when it detects a path mismatch via the new areEquivalentStoragePaths helper (windows-aware), and marking transactions inactive in a finally block so stale context can't leak to post-transaction reads.

  • normalizeStorageComparisonPath / areEquivalentStoragePaths: correct design for windows case-insensitivity, but uses path.resolve not fs.realpath — symlink aliases (e.g. macos /var → /private/var) can produce a false mismatch and cause exportAccounts to throw a spurious error against valid token storage
  • latestValidSnapshot gains a stable index tiebreaker when mtimeMs values are equal — clean fix, well tested
  • state.active = false in finally for both transaction functions — correct; prevents stale snapshot from being visible to exportAccounts called after a throw
  • missing vitest coverage: no path-drift+export fast-fail test for withAccountAndFlaggedStorageTransaction; the windows replaceAll("\\", "/") branch is never actually exercised on posix ci since path.resolve never emits backslashes on linux

Confidence Score: 3/5

  • safe to merge with low-severity risk: the symlink alias edge case could cause a spurious export error for users on macos with symlinked storage paths (e.g. /var/folders), but won't silently corrupt token data
  • core path-pinning and fast-fail logic is correct and well-covered; the symlink gap in normalizeStorageComparisonPath is a p1 logic issue that can throw a false path-mismatch error against valid storage (token safety risk on macos), and two vitest coverage gaps remain (combined-transaction drift+export, windows separator normalization)
  • lib/storage.ts — normalizeStorageComparisonPath (symlink resolution); test/storage.test.ts — missing withAccountAndFlaggedStorageTransaction path-drift export test

Important Files Changed

Filename Overview
lib/storage.ts adds path-pinning to loadAccountsInternal/saveAccountsUnlocked, fast-fail in exportAccounts, and a stable tiebreaker in latestValidSnapshot — logic is sound but normalizeStorageComparisonPath uses path.resolve (not fs.realpath) so symlink aliases can produce a false mismatch and throw a spurious export error
test/storage.test.ts good coverage of the new paths: path-drift fast-fail, windows case equivalence, active-deactivation on success/throw for both transaction variants, and the mtime tiebreaker; gaps: no equivalent path-drift+export test for withAccountAndFlaggedStorageTransaction, and the windows backslash separator branch is never exercised on linux ci

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[withAccountStorageTransaction / withAccountAndFlaggedStorageTransaction] --> B[withStorageLock]
    B --> C[storagePath = getStoragePath at start]
    C --> D[loadAccountsInternal pinned to storagePath]
    D --> E[transactionSnapshotContext.run state active=true]
    E --> F[handler current persist]
    F -->|persist called| G[saveAccountsUnlocked pinned to storagePath]
    G --> H[state.snapshot updated]
    F -->|success or throw| I[finally: state.active = false]
    I --> J[transaction done]

    K[exportAccounts called] --> L{transactionState?.active?}
    L -->|no| M[new withAccountStorageTransaction fresh load]
    L -->|yes| N{areEquivalentStoragePaths transaction vs active?}
    N -->|paths match| O[use transactionState.snapshot]
    N -->|paths differ| P[throw path mismatch error]

    style P fill:#f55,color:#fff
    style O fill:#5a5,color:#fff
    style M fill:#5a5,color:#fff
Loading

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: test/storage.test.ts
Line: 961-1001

Comment:
**missing path-drift export test for combined transaction**

the pr adds a "fails fast" test for `withAccountStorageTransaction` + path drift, but there's no equivalent test for `withAccountAndFlaggedStorageTransaction`. both functions have the same `areEquivalentStoragePaths` fast-fail in `exportAccounts`, and the same `finally { state.active = false }` guard, but only the simple transaction variant is covered for the path-drift-then-export scenario. this is a coverage gap — a regression to the combined transaction variant would go undetected.

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

---

This is a comment left during a code review.
Path: test/storage.test.ts
Line: 1003-1050

Comment:
**windows separator normalization is not tested on linux ci**

the test patches `process.platform` to `"win32"` and exercises the **case-insensitive** branch via `.toUpperCase()`. on a posix host, `path.resolve()` never produces backslash separators, so the `replaceAll("\\", "/")` half of `normalizeStorageComparisonPath` is never actually exercised. a path like `C:\\Users\\foo\\accounts.json` is the true windows risk — mixing forward and back slashes in the stored vs active path string — and that's untested. worth adding a unit-level test that directly calls `areEquivalentStoragePaths` with mixed-separator strings on a spoofed win32 platform to close this gap.

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

---

This is a comment left during a code review.
Path: lib/storage.ts
Line: 384-396

Comment:
**symlink aliases can fool path equivalence on posix**

`normalizeStorageComparisonPath` calls `resolvePath` (which uses `path.resolve`) — that resolves `.` and `..` components but does **not** dereference symlinks. if the stored transaction path was captured through a symlink (`~/.codex/multi-auth → /Users/foo/.codex/multi-auth`) and the active path comes in as the real path (or vice versa), `areEquivalentStoragePaths` returns `false` and `exportAccounts` throws a spurious mismatch error. on windows token storage paths this is less common, but on macos `/var``/private/var` symlinks are a well-known trap. using `fs.realpathSync` (with a try/catch for missing paths) would make the comparison robust. worth at least a code comment noting the symlink caveat.

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

Last reviewed commit: "Harden storage trans..."

Greptile also left 1 inline comment on this PR.

@chatgpt-codex-connector
Copy link

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
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Warning

Rate limit exceeded

@ndycode has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 16 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1804d9bd-df22-433d-8adb-ff0aca8b8d29

📥 Commits

Reviewing files that changed from the base of the PR and between 1be5e95 and 404a930.

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

Walkthrough

adds platform-aware storage path normalization and comparison utilities to detect path drift during transactions. transaction handlers now capture storage path in state and validate it at export time, preventing exports when paths change. introduces new optional parameters to internal functions to support custom storage paths.

Changes

Cohort / File(s) Summary
Path normalization utilities
lib/storage.ts
added normalizeStorageComparisonPath() and areEquivalentStoragePaths() for cross-platform path comparison with Windows case-insensitivity and backslash-to-forward-slash normalization.
Snapshot selection stability
lib/storage.ts
refactored latestValidSnapshot() to use stable tie-breaking by original index, preventing undefined returns and ensuring deterministic snapshot selection when mtimes match.
Transaction state management
lib/storage.ts
modified withAccountStorageTransaction() and withAccountAndFlaggedStorageTransaction() to capture storagePath in transaction state and clear state.active in finally block; updated loadAccountsInternal() and saveAccountsUnlocked() to accept optional storagePath parameter.
Export validation
lib/storage.ts
strengthened exportAccounts() to reject exports when transaction's storagePath diverges from currently active path via areEquivalentStoragePaths() comparison instead of strict === check.
Transaction path drift test coverage
test/storage.test.ts
added comprehensive test cases validating that exports fail on storage path mismatch, succeed on equivalent Windows formatting, reload storage after handler exceptions, and persist transaction updates to original paths; includes backup metadata snapshot selection tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

bug


detailed notes for review

windows path edge cases: carefully verify normalizeStorageComparisonPath() at lib/storage.ts handles all combinations—drive letter casing, mixed slashes, relative vs. absolute paths. tests at test/storage.test.ts include win32 platform simulation but check that non-win32 systems don't over-normalize.

transaction state lifecycle: the finally block clearing state.active in transaction wrappers is critical; ensure this executes even if the handler throws or if loadAccountsInternal()/saveAccountsUnlocked() encounter errors. verify storagePath parameter threading through both transaction variants is consistent.

export blocking logic: exportAccounts() now blocks on path mismatch; confirm the error message is user-friendly and that the check happens before any file writes. review how the force parameter interacts with the new path equivalence check.

snapshot tie-breaking: latestValidSnapshot() now sorts by mtimeMs descending, then original index ascending. the test at test/storage.test.ts asserts preference for .storagePath over .bak when mtimes are equal; confirm the snapshot ordering in real backup scenarios matches intent.

regression test coverage: tests cover exception handling, storage reloads, and combined transactions, but verify there's no gap for concurrent transaction attempts or race conditions between path checks and file operations.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch git-clean/05-storage-hardening
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch git-clean/05-storage-hardening

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
Copy link
Owner Author

ndycode commented Mar 20, 2026

@coderabbitai review
@greptileai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

@ndycode sure, kicking off a full review of PR #145 now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ndycode ndycode closed this Mar 20, 2026
@ndycode ndycode deleted the git-clean/05-storage-hardening branch March 20, 2026 19:01
Comment on lines +384 to +396
function normalizeStorageComparisonPath(path: string): string {
const resolved = resolvePath(path);
if (process.platform !== "win32") {
return resolved;
}
return resolved.replaceAll("\\", "/").toLowerCase();
}

function areEquivalentStoragePaths(left: string, right: string): boolean {
return (
normalizeStorageComparisonPath(left) === normalizeStorageComparisonPath(right)
);
}
Copy link

Choose a reason for hiding this comment

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

P1 symlink aliases can fool path equivalence on posix

normalizeStorageComparisonPath calls resolvePath (which uses path.resolve) — that resolves . and .. components but does not dereference symlinks. if the stored transaction path was captured through a symlink (~/.codex/multi-auth → /Users/foo/.codex/multi-auth) and the active path comes in as the real path (or vice versa), areEquivalentStoragePaths returns false and exportAccounts throws a spurious mismatch error. on windows token storage paths this is less common, but on macos /var/private/var symlinks are a well-known trap. using fs.realpathSync (with a try/catch for missing paths) would make the comparison robust. worth at least a code comment noting the symlink caveat.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/storage.ts
Line: 384-396

Comment:
**symlink aliases can fool path equivalence on posix**

`normalizeStorageComparisonPath` calls `resolvePath` (which uses `path.resolve`) — that resolves `.` and `..` components but does **not** dereference symlinks. if the stored transaction path was captured through a symlink (`~/.codex/multi-auth → /Users/foo/.codex/multi-auth`) and the active path comes in as the real path (or vice versa), `areEquivalentStoragePaths` returns `false` and `exportAccounts` throws a spurious mismatch error. on windows token storage paths this is less common, but on macos `/var``/private/var` symlinks are a well-known trap. using `fs.realpathSync` (with a try/catch for missing paths) would make the comparison robust. worth at least a code comment noting the symlink caveat.

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

Fix in Codex

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