-
Notifications
You must be signed in to change notification settings - Fork 0
feat: complete enterprise hardening and storage lock reliability #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/enterprise-hardening
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -109,6 +109,7 @@ export function formatStorageErrorHint(error: unknown, path: string): string { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let storageMutex: Promise<void> = Promise.resolve(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let accountFileMutex: Promise<void> = Promise.resolve(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function withStorageLock<T>(fn: () => Promise<T>): Promise<T> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const previousMutex = storageMutex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -119,6 +120,24 @@ function withStorageLock<T>(fn: () => Promise<T>): Promise<T> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return previousMutex.then(fn).finally(() => releaseLock()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function withAccountFileMutex<T>(fn: () => Promise<T>): Promise<T> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const previousMutex = accountFileMutex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let releaseLock: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accountFileMutex = new Promise<void>((resolve) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| releaseLock = resolve; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return previousMutex.then(fn).finally(() => releaseLock()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function withStorageSerializedFileLock<T>(path: string, fn: () => Promise<T>): Promise<T> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Serialize file-lock acquisition to keep save ordering deterministic, then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // preserve the historical lock order (file lock -> in-process mutex) so all | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // account-storage mutation paths share the same acquisition sequence. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return withAccountFileMutex(() => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| withAccountFileLock(path, () => withStorageLock(fn)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+123
to
+139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lock order comment contradicts actual implementation. the comment at line 133-135 says "preserve the historical lock order (file lock -> in-process mutex)" but the actual acquisition order is: if the intent was to reverse lock order (as pr description suggests), update the comment. if the comment is correct, the code is wrong. - // Serialize file-lock acquisition to keep save ordering deterministic, then
- // preserve the historical lock order (file lock -> in-process mutex) so all
- // account-storage mutation paths share the same acquisition sequence.
+ // Serialize file-lock acquisition to keep save ordering deterministic.
+ // Acquisition order: in-process mutex -> file lock -> storage mutex.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type AnyAccountStorage = AccountStorageV1 | AccountStorageV3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type AccountLike = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -318,16 +337,70 @@ function getAccountsLockPath(path: string): string { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `${path}.lock`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function releaseStorageLockFallback(lockPath: string): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await fs.rm(lockPath, { force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Best-effort lock cleanup fallback. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+340
to
+346
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial
using consider logging at debug level for observability: async function releaseStorageLockFallback(lockPath: string): Promise<void> {
try {
await fs.rm(lockPath, { force: true });
- } catch {
- // Best-effort lock cleanup fallback.
+ } catch (error) {
+ log.debug("best-effort lock cleanup failed", { path: lockPath, error: String(error) });
}
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function cleanupDeadProcessStorageLock(lockPath: string): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const raw = await fs.readFile(lockPath, "utf-8"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = JSON.parse(raw) as { pid?: number; acquiredAt?: number }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lockPid = Number(parsed?.pid); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lockAcquiredAt = Number(parsed?.acquiredAt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (Number.isFinite(lockPid) && lockPid > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let isDeadProcess = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.kill(lockPid, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const code = (error as NodeJS.ErrnoException).code; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isDeadProcess = code === "ESRCH"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+355
to
+362
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add observability for dead-process lock cleanup. when a stale lock from a dead process is cleaned up (line 365), there's no logging. this conflicts with the ops runbook expectations mentioned in the pr description. operators need visibility into when stale locks are being cleaned. if (isDeadProcess) {
+ log.info("cleaning up stale lock from dead process", { path: lockPath, pid: lockPid });
await releaseStorageLockFallback(lockPath);
return;
}similarly for age-based cleanup at line 371: if (Number.isFinite(lockAcquiredAt) && Date.now() - lockAcquiredAt > ACCOUNT_STORAGE_LOCK_OPTIONS.staleAfterMs) {
+ log.info("cleaning up stale lock by age", { path: lockPath, acquiredAt: lockAcquiredAt, ageMs: Date.now() - lockAcquiredAt });
await releaseStorageLockFallback(lockPath);
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isDeadProcess) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await releaseStorageLockFallback(lockPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (Number.isFinite(lockAcquiredAt) && Date.now() - lockAcquiredAt > ACCOUNT_STORAGE_LOCK_OPTIONS.staleAfterMs) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await releaseStorageLockFallback(lockPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const code = (error as NodeJS.ErrnoException).code; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (code === "ENOENT") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await releaseStorageLockFallback(lockPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+373
to
+379
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
when consider only calling fallback cleanup for parse errors, not for } catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
return;
}
+ if (code === "EBUSY") {
+ // another process is actively using this lock
+ return;
+ }
await releaseStorageLockFallback(lockPath);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+347
to
+380
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. non-atomic TOCTOU sequence in dead process detection. between reading lock file, checking PID, and removing lock, the legitimate owner could still be using it or another process could recreate it. additionally, windows filesystem concern: if antivirus locks the file between stat check and unlink, this could thrash or leave stale locks. Prompt To Fix With AIThis is a comment left during a code review.
Path: lib/storage.ts
Line: 347-380
Comment:
non-atomic TOCTOU sequence in dead process detection. between reading lock file, checking PID, and removing lock, the legitimate owner could still be using it or another process could recreate it.
additionally, `process.kill(lockPid, 0)` fails across user boundaries and in some container environments where PIDs are namespaced.
windows filesystem concern: if antivirus locks the file between stat check and unlink, this could thrash or leave stale locks.
How can I resolve this? If you propose a fix, please make it concise.
Comment on lines
+348
to
+380
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: On Windows, What the two common errors mean
Practical interpretationSo on Windows, for
Sources: Node.js docs for 🏁 Script executed: # Find tests for cleanupDeadProcessStorageLock
rg -i "cleanupDeadProcessStorageLock" --type ts --type js
# Check test files in the repository
git ls-files test/ lib/ | head -20Repository: ndycode/codex-multi-auth Length of output: 650 🏁 Script executed: # Find test files for storage
fd -e test.ts -e spec.ts | rg -i storage
# Also search for any test files
fd -e test.ts -e spec.ts | head -20Repository: ndycode/codex-multi-auth Length of output: 750 🏁 Script executed: # Check for logging in the storage.ts file and around the function
rg "logger\.|log\(" lib/storage.ts -B 2 -A 2
# Look at broader context around cleanupDeadProcessStorageLock
sed -n '320,400p' lib/storage.tsRepository: ndycode/codex-multi-auth Length of output: 2509 🏁 Script executed: # Search for tests of cleanupDeadProcessStorageLock in test files
rg "cleanupDeadProcessStorageLock" test/
# Also check what the storage test files cover
rg -A 10 -B 2 "process\.kill\|deadProcess" test/storage*.test.tsRepository: ndycode/codex-multi-auth Length of output: 50 🏁 Script executed: # Check logging imports in storage.ts
head -50 lib/storage.ts | rg "import.*log"Repository: ndycode/codex-multi-auth Length of output: 111 🏁 Script executed: # Check for log variable definition in storage.ts
rg "^const log|^let log" lib/storage.ts
# Check entire test/storage.test.ts to see what functions ARE tested
head -100 test/storage.test.tsRepository: ndycode/codex-multi-auth Length of output: 3239 add logging to the also, this critical lock management function
without logging and tests here, you won't know when lock cleanup fails in the field. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function withAccountFileLock<T>(path: string, fn: () => Promise<T>): Promise<T> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lockPath = getAccountsLockPath(path); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await cleanupDeadProcessStorageLock(lockPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await fs.mkdir(dirname(path), { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lock = await acquireFileLock(getAccountsLockPath(path), ACCOUNT_STORAGE_LOCK_OPTIONS); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lock = await acquireFileLock(lockPath, ACCOUNT_STORAGE_LOCK_OPTIONS); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
382
to
+386
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial cleanup before acquire creates race window. calling consider moving cleanup inside 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await fn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await lock.release(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await lock.release(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const code = (error as NodeJS.ErrnoException).code; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (code !== "ENOENT") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.warn("Failed to release account storage lock", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: lockPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: String(error), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await releaseStorageLockFallback(lockPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+393
to
+401
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. race condition: should only call fallback if
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: lib/storage.ts
Line: 393-401
Comment:
race condition: `releaseStorageLockFallback` executes unconditionally after successful `lock.release()`. between the unlink in `release()` and this fallback call, another process can acquire the lock. the fallback then removes that process's valid lock file, breaking cross-process serialization.
should only call fallback if `lock.release()` threw a non-ENOENT error:
```suggestion
try {
await lock.release();
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code !== "ENOENT") {
log.warn("Failed to release account storage lock", {
path: lockPath,
error: String(error),
});
await releaseStorageLockFallback(lockPath);
}
}
}
```
How can I resolve this? If you propose a fix, please make it concise.
Comment on lines
+390
to
+401
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial double cleanup: both the finally block at lines 390-401 calls this seems intentional for robustness but could hide issues. consider only calling fallback when release actually fails: } finally {
+ let releaseSucceeded = false;
try {
await lock.release();
+ releaseSucceeded = true;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code !== "ENOENT") {
log.warn("Failed to release account storage lock", {
path: lockPath,
error: String(error),
});
}
}
- await releaseStorageLockFallback(lockPath);
+ if (!releaseSucceeded) {
+ await releaseStorageLockFallback(lockPath);
+ }
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function copyFileWithRetry( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sourcePath: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| destinationPath: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1242,14 +1315,13 @@ export async function withAccountStorageTransaction<T>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) => Promise<T>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<T> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const path = getStoragePath(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return withAccountFileLock(path, () => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| withStorageLock(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const current = await loadAccountsInternal(saveAccountsUnlocked); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return handler(current, saveAccountsUnlocked); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return withStorageSerializedFileLock(path, async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const current = await loadAccountsInternal(saveAccountsUnlocked); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return handler(current, saveAccountsUnlocked); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Persists account storage to disk using atomic write (temp file + rename). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Creates the Codex multi-auth storage directory if it doesn't exist. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1259,23 +1331,21 @@ export async function withAccountStorageTransaction<T>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function saveAccounts(storage: AccountStorageV3): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const path = getStoragePath(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return withAccountFileLock(path, () => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| withStorageLock(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await saveAccountsUnlocked(storage); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return withStorageSerializedFileLock(path, async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await saveAccountsUnlocked(storage); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Deletes the account storage file from disk. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Silently ignores if file doesn't exist. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function clearAccounts(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const path = getStoragePath(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return withAccountFileLock(path, () => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| withStorageLock(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const walPath = getAccountsWalPath(path); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const backupPaths = getAccountsBackupRecoveryCandidates(path); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return withStorageSerializedFileLock(path, async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const walPath = getAccountsWalPath(path); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const backupPaths = getAccountsBackupRecoveryCandidates(path); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const clearPath = async (targetPath: string): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await fs.unlink(targetPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1295,10 +1365,10 @@ export async function clearAccounts(): Promise<void> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Individual path cleanup is already best-effort with per-artifact logging. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function normalizeFlaggedStorage(data: unknown): FlaggedAccountStorageV1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isRecord(data) || data.version !== 1 || !Array.isArray(data.accounts)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { version: 1, accounts: [] }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1597,3 +1667,5 @@ export async function rotateStoredSecretEncryption(): Promise<{ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flaggedAccounts: flaggedCount, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.