11import { createHash } from "node:crypto" ;
22import { closeSync , existsSync , readFileSync , mkdirSync , openSync , unlinkSync , writeFileSync } from "node:fs" ;
33import { join } from "node:path" ;
4- import { getConfigDir , atomicWriteFile , backupInvalidConfig , hardenConfigDir , hardenExistingSecret } from "../config" ;
4+ import {
5+ ConfigMutationLockError ,
6+ getConfigDir ,
7+ atomicWriteFile ,
8+ backupInvalidConfig ,
9+ hardenConfigDir ,
10+ hardenExistingSecret ,
11+ withConfigMutationLockSync ,
12+ } from "../config" ;
513import { assertNotRealHomeUnderTest } from "../lib/test-home-guard" ;
614import type { CodexAccountCredentialRecord , CodexAccountCredentials } from "../types" ;
715
@@ -121,44 +129,50 @@ export function getCodexAccountCredential(id: string): CodexAccountCredentials |
121129}
122130
123131export function saveCodexAccountCredential ( id : string , cred : CodexAccountCredentials ) : void {
124- const store = loadCodexAccountRecordStore ( ) ;
125- const current = store [ id ] ;
126- const refreshGrantFingerprint = current ?. credential ?. refreshToken === cred . refreshToken
127- ? current . refreshGrantFingerprint ?? refreshGrantFingerprintForToken ( cred . refreshToken )
128- : refreshGrantFingerprintForToken ( cred . refreshToken ) ;
129- store [ id ] = {
130- credential : cred ,
131- generation : ( current ?. generation ?? 0 ) + 1 ,
132- refreshGrantFingerprint,
133- replacedAt : current ? Date . now ( ) : undefined ,
134- ...preservedValidationMetadata ( current ) ,
135- } ;
136- persist ( store ) ;
132+ withCredentialMutationLockSync ( ( ) => {
133+ const store = loadCodexAccountRecordStore ( ) ;
134+ const current = store [ id ] ;
135+ const refreshGrantFingerprint = current ?. credential ?. refreshToken === cred . refreshToken
136+ ? current . refreshGrantFingerprint ?? refreshGrantFingerprintForToken ( cred . refreshToken )
137+ : refreshGrantFingerprintForToken ( cred . refreshToken ) ;
138+ store [ id ] = {
139+ credential : cred ,
140+ generation : ( current ?. generation ?? 0 ) + 1 ,
141+ refreshGrantFingerprint,
142+ replacedAt : current ? Date . now ( ) : undefined ,
143+ ...preservedValidationMetadata ( current ) ,
144+ } ;
145+ persist ( store ) ;
146+ } ) ;
137147}
138148
139149export function markCodexAccountValidated ( id : string , atMs : number = Date . now ( ) ) : void {
140- const store = loadCodexAccountRecordStore ( ) ;
141- const current = store [ id ] ;
142- if ( ! current || current . deletedAt != null || ! current . credential ) return ;
143- store [ id ] = {
144- ...current ,
145- lastCodexValidatedAt : atMs ,
146- lastCodexValidationStatus : "ok" ,
147- lastCodexValidationError : undefined ,
148- } ;
149- persist ( store ) ;
150+ withCredentialMutationLockSync ( ( ) => {
151+ const store = loadCodexAccountRecordStore ( ) ;
152+ const current = store [ id ] ;
153+ if ( ! current || current . deletedAt != null || ! current . credential ) return ;
154+ store [ id ] = {
155+ ...current ,
156+ lastCodexValidatedAt : atMs ,
157+ lastCodexValidationStatus : "ok" ,
158+ lastCodexValidationError : undefined ,
159+ } ;
160+ persist ( store ) ;
161+ } ) ;
150162}
151163
152164export function markCodexAccountValidationFailed ( id : string , reason : string ) : void {
153- const store = loadCodexAccountRecordStore ( ) ;
154- const current = store [ id ] ;
155- if ( ! current || current . deletedAt != null || ! current . credential ) return ;
156- store [ id ] = {
157- ...current ,
158- lastCodexValidationStatus : "failed" ,
159- lastCodexValidationError : reason ,
160- } ;
161- persist ( store ) ;
165+ withCredentialMutationLockSync ( ( ) => {
166+ const store = loadCodexAccountRecordStore ( ) ;
167+ const current = store [ id ] ;
168+ if ( ! current || current . deletedAt != null || ! current . credential ) return ;
169+ store [ id ] = {
170+ ...current ,
171+ lastCodexValidationStatus : "failed" ,
172+ lastCodexValidationError : reason ,
173+ } ;
174+ persist ( store ) ;
175+ } ) ;
162176}
163177
164178export function removeCodexAccountCredential ( id : string ) : void {
@@ -183,32 +197,36 @@ export function saveCodexAccountCredentialIfGeneration(
183197 generation : number ,
184198 cred : CodexAccountCredentials ,
185199) : boolean {
186- const store = loadCodexAccountRecordStore ( ) ;
187- const current = store [ id ] ;
188- if ( ! current || current . generation !== generation || current . deletedAt != null || ! current . credential ) {
189- return false ;
190- }
191- const refreshGrantFingerprint = current . credential . refreshToken === cred . refreshToken
192- ? current . refreshGrantFingerprint ?? refreshGrantFingerprintForToken ( cred . refreshToken )
193- : refreshGrantFingerprintForToken ( cred . refreshToken ) ;
194- store [ id ] = {
195- credential : cred ,
196- generation : generation + 1 ,
197- refreshGrantFingerprint,
198- replacedAt : current . replacedAt ,
199- ...preservedValidationMetadata ( current ) ,
200- } ;
201- persist ( store ) ;
202- return true ;
200+ return withCredentialMutationLockSync ( ( ) => {
201+ const store = loadCodexAccountRecordStore ( ) ;
202+ const current = store [ id ] ;
203+ if ( ! current || current . generation !== generation || current . deletedAt != null || ! current . credential ) {
204+ return false ;
205+ }
206+ const refreshGrantFingerprint = current . credential . refreshToken === cred . refreshToken
207+ ? current . refreshGrantFingerprint ?? refreshGrantFingerprintForToken ( cred . refreshToken )
208+ : refreshGrantFingerprintForToken ( cred . refreshToken ) ;
209+ store [ id ] = {
210+ credential : cred ,
211+ generation : generation + 1 ,
212+ refreshGrantFingerprint,
213+ replacedAt : current . replacedAt ,
214+ ...preservedValidationMetadata ( current ) ,
215+ } ;
216+ persist ( store ) ;
217+ return true ;
218+ } ) ;
203219}
204220
205221export function tombstoneCodexAccount ( id : string ) : number {
206- const store = loadCodexAccountRecordStore ( ) ;
207- const current = store [ id ] ;
208- const generation = ( current ?. generation ?? 0 ) + 1 ;
209- store [ id ] = { generation, deletedAt : Date . now ( ) } ;
210- persist ( store ) ;
211- return generation ;
222+ return withCredentialMutationLockSync ( ( ) => {
223+ const store = loadCodexAccountRecordStore ( ) ;
224+ const current = store [ id ] ;
225+ const generation = ( current ?. generation ?? 0 ) + 1 ;
226+ store [ id ] = { generation, deletedAt : Date . now ( ) } ;
227+ persist ( store ) ;
228+ return generation ;
229+ } ) ;
212230}
213231
214232const CHATGPT_TOKEN_URL = "https://auth.openai.com/oauth/token" ;
@@ -237,6 +255,16 @@ export class CodexCredentialRefreshLockTimeoutError extends Error {
237255 }
238256}
239257
258+ /** Credential writers share the config mutation coordinator; contention is transient, not reauth. */
259+ function withCredentialMutationLockSync < T > ( fn : ( ) => T ) : T {
260+ try {
261+ return withConfigMutationLockSync ( fn ) ;
262+ } catch ( error ) {
263+ if ( error instanceof ConfigMutationLockError ) throw new CodexCredentialRefreshLockTimeoutError ( ) ;
264+ throw error ;
265+ }
266+ }
267+
240268type CodexTokenResult = { accessToken : string ; chatgptAccountId : string ; generation : number } ;
241269type CodexRefreshResult = CodexTokenResult & { credential ?: CodexAccountCredentials } ;
242270const refreshLocks = new Map < string , Promise < CodexRefreshResult > > ( ) ;
0 commit comments