11import { createHash } from "node:crypto" ;
2- import {
3- mkdir ,
4- open ,
5- readFile ,
6- rename ,
7- stat ,
8- unlink ,
9- writeFile ,
10- } from "node:fs/promises" ;
11- import { readFileSync , writeFileSync } from "node:fs" ;
2+ import { mkdir , readFile , rename , unlink , writeFile } from "node:fs/promises" ;
3+ import { writeFileSync } from "node:fs" ;
124import { homedir } from "node:os" ;
135import { dirname , join } from "node:path" ;
14- import { setTimeout as delay } from "node:timers/promises" ;
156import { getLogger } from "@intx/log" ;
167import type {
178 OAuthClientInformationFull ,
@@ -116,15 +107,6 @@ function stateFromRaw(raw: string | undefined, path: string): MCPAuthState {
116107 return parseAuthState ( raw , path ) ?? { } ;
117108}
118109
119- function readAuthFileSync ( path : string ) : string | undefined {
120- try {
121- return readFileSync ( path , "utf8" ) ;
122- } catch ( err ) {
123- if ( isEnoent ( err ) ) return undefined ;
124- throw err ;
125- }
126- }
127-
128110async function readAuthFile ( path : string ) : Promise < string | undefined > {
129111 try {
130112 return await readFile ( path , "utf8" ) ;
@@ -142,97 +124,18 @@ export async function loadAuthState(
142124 return stateFromRaw ( await readAuthFile ( path ) , path ) ;
143125}
144126
145- // Cache refresh: missing/unreadable/corrupt files return undefined so the
146- // caller keeps its in-memory mirror. Empty-on-corrupt is loadAuthState's contract.
147- export function tryLoadAuthStateSync (
148- identity : MCPAuthIdentity ,
149- home : string = homedir ( ) ,
150- ) : MCPAuthState | undefined {
151- const path = authFilePath ( identity , home ) ;
152- let raw : string | undefined ;
153- try {
154- raw = readAuthFileSync ( path ) ;
155- } catch {
156- return undefined ;
157- }
158- if ( raw === undefined ) return undefined ;
159- return parseAuthState ( raw , path ) ;
160- }
161-
162- function isEexist ( err : unknown ) : boolean {
163- return (
164- typeof err === "object" &&
165- err !== null &&
166- "code" in err &&
167- err . code === "EEXIST"
168- ) ;
169- }
170-
171127// pid alone is not unique per call, so concurrent saves must not share a temp
172128// path or the second rename hits ENOENT.
173129let tmpWriteCounter = 0 ;
174130
175131// Serialize read-modify-write per auth file so two providers for the same
176- // server cannot clobber each other's fields (lost-update).
132+ // server cannot clobber each other's fields (lost-update). Same-process only:
133+ // the chain runs op directly, with no cross-process file lock.
177134const updateChains = new Map < string , Promise < unknown > > ( ) ;
178135
179- const LOCK_STALE_MS = 5_000 ;
180- const LOCK_RETRY_MS = 25 ;
181-
182- async function acquireAuthFileLock ( lockPath : string ) {
183- while ( true ) {
184- try {
185- return await open ( lockPath , "wx" , 0o600 ) ;
186- } catch ( err ) {
187- if ( ! isEexist ( err ) ) throw err ;
188- try {
189- const info = await stat ( lockPath ) ;
190- if ( Date . now ( ) - info . mtimeMs > LOCK_STALE_MS ) {
191- try {
192- await unlink ( lockPath ) ;
193- } catch ( unlinkErr ) {
194- if ( ! isEnoent ( unlinkErr ) ) throw unlinkErr ;
195- }
196- continue ;
197- }
198- } catch ( statErr ) {
199- if ( isEnoent ( statErr ) ) continue ;
200- throw statErr ;
201- }
202- await delay ( LOCK_RETRY_MS ) ;
203- }
204- }
205- }
206-
207- async function withAuthFileLock < T > (
208- path : string ,
209- op : ( ) => Promise < T > ,
210- ) : Promise < T > {
211- const lockPath = `${ path } .lock` ;
212- await mkdir ( dirname ( path ) , { recursive : true , mode : 0o700 } ) ;
213- const lock = await acquireAuthFileLock ( lockPath ) ;
214- try {
215- return await op ( ) ;
216- } finally {
217- try {
218- await lock . close ( ) ;
219- } catch {
220- // Close can fail if the handle was already torn down.
221- }
222- try {
223- await unlink ( lockPath ) ;
224- } catch {
225- // Missing lock is fine; a leftover file is recovered as stale.
226- }
227- }
228- }
229-
230136function enqueueAuthFileOp < T > ( path : string , op : ( ) => Promise < T > ) : Promise < T > {
231137 const previous = updateChains . get ( path ) ?? Promise . resolve ( ) ;
232- const run = previous . then (
233- ( ) => withAuthFileLock ( path , op ) ,
234- ( ) => withAuthFileLock ( path , op ) ,
235- ) ;
138+ const run = previous . then ( op , op ) ;
236139 updateChains . set (
237140 path ,
238141 run . then (
0 commit comments