@@ -18,7 +18,8 @@ export type { AuthProfile, BaseTokens };
1818// for the same provider, so credentials are keyed by a user-chosen profile name
1919// within a single file. Tokens are credentials, so the file is owner-only (0o600)
2020// and the directory 0o700. Writes go through a temp file + rename so a concurrent
21- // reader never observes a torn file.
21+ // reader never observes a torn file. Same-process writers also queue per auth path
22+ // so each lock wait starts its own deadline.
2223
2324export interface AuthStore < TTokens extends BaseTokens > {
2425 authPath : ( home ?: string ) => string ;
@@ -48,6 +49,15 @@ interface AuthFile<TTokens extends BaseTokens> {
4849const LOCK_RETRY_MS = 25 ;
4950const LOCK_TIMEOUT_MS = 1_000 ;
5051
52+ // Per-call unique temp (pid + counter). Matches mcp/auth-store — pid alone is not
53+ // unique per call if writeAuthFile ever overlaps in-process.
54+ let tmpWriteCounter = 0 ;
55+
56+ // Same-process ops on one auth file queue here so a caller's lock deadline
57+ // starts when it actually runs, not when it was invoked — otherwise one lock
58+ // held past LOCK_TIMEOUT_MS fails the whole burst, not just the first waiter.
59+ const updateChains = new Map < string , Promise < unknown > > ( ) ;
60+
5161const AuthFileShape = type ( {
5262 profiles : "Record<string, unknown>" ,
5363} ) ;
@@ -115,7 +125,7 @@ export function createAuthStore<TTokens extends BaseTokens>(
115125 ) : Promise < void > {
116126 const path = authPath ( home ) ;
117127 await mkdir ( dirname ( path ) , { recursive : true , mode : 0o700 } ) ;
118- const tmp = `${ path } .${ String ( process . pid ) } .tmp` ;
128+ const tmp = `${ path } .${ process . pid } . ${ ( tmpWriteCounter += 1 ) } .tmp` ;
119129 await writeFile ( tmp , JSON . stringify ( file , null , 2 ) , { mode : 0o600 } ) ;
120130 await rename ( tmp , path ) ;
121131 }
@@ -158,6 +168,26 @@ export function createAuthStore<TTokens extends BaseTokens>(
158168 }
159169 }
160170
171+ function enqueueAuthFileOp < TResult > (
172+ home : string ,
173+ op : ( ) => Promise < TResult > ,
174+ ) : Promise < TResult > {
175+ const path = authPath ( home ) ;
176+ const previous = updateChains . get ( path ) ?? Promise . resolve ( ) ;
177+ const run = previous . then (
178+ ( ) => withAuthFileLock ( home , op ) ,
179+ ( ) => withAuthFileLock ( home , op ) ,
180+ ) ;
181+ updateChains . set (
182+ path ,
183+ run . then (
184+ ( ) => undefined ,
185+ ( ) => undefined ,
186+ ) ,
187+ ) ;
188+ return run ;
189+ }
190+
161191 return {
162192 authPath,
163193 async listProfiles (
@@ -179,7 +209,7 @@ export function createAuthStore<TTokens extends BaseTokens>(
179209 profile : AuthProfile < TTokens > ,
180210 home : string = homedir ( ) ,
181211 ) : Promise < void > {
182- await withAuthFileLock ( home , async ( ) => {
212+ await enqueueAuthFileOp ( home , async ( ) => {
183213 const file = await readAuthFile ( home ) ;
184214 file . profiles [ profile . name ] = profile ;
185215 await writeAuthFile ( file , home ) ;
@@ -192,7 +222,7 @@ export function createAuthStore<TTokens extends BaseTokens>(
192222 tokens : TTokens ,
193223 home : string = homedir ( ) ,
194224 ) : Promise < void > {
195- await withAuthFileLock ( home , async ( ) => {
225+ await enqueueAuthFileOp ( home , async ( ) => {
196226 const file = await readAuthFile ( home ) ;
197227 const existing = file . profiles [ name ] ;
198228 if ( existing === undefined ) return ;
@@ -204,7 +234,7 @@ export function createAuthStore<TTokens extends BaseTokens>(
204234 name : string | undefined ,
205235 home : string = homedir ( ) ,
206236 ) : Promise < string [ ] > {
207- return withAuthFileLock ( home , async ( ) => {
237+ return enqueueAuthFileOp ( home , async ( ) => {
208238 const file = await readAuthFile ( home ) ;
209239 if ( name === undefined ) {
210240 const removed = Object . keys ( file . profiles ) ;
0 commit comments