Follow-up from cli#231 / cli#235 (config locking), characterized by the #235 adversarial verifier: 200 iterations of a sign-out (deleteAuth) racing an in-flight token refresh (mutateAuth) -> 94 sign-out-won / 106 resurrected / 0 corrupt.
Root cause (deeper than deleteAuth)
deleteAuth is lock-free, but making it locked only serializes the two ops - it does NOT stop resurrection. The actual cause: when a token refresh's mutateAuth re-reads a null (just-signed-out) auth.json, tryRefresh (src/lib/api.ts) falls back to the IN-MEMORY tokens and writes them back, resurrecting the session the user just ended.
Correct fix
Two parts:
- Route
deleteAuth through withFileLock (make it async; the single caller setup.ts disconnect already awaits) - removes the torn window, deterministic ordering.
- In the refresh path, treat "disk read is null" as "signed out - do not resurrect":
mutateAuth's refresh mutator should return null (skip the write) when the fresh disk read is null, instead of folding in-memory tokens. This is the part that actually kills resurrection.
Severity
LOW: no corruption, atomic writes throughout, resurrected content is the user's own freshly-refreshed valid tokens (never stale/foreign), sub-ms window, and it requires an explicit --disconnect racing a background refresh. The lost-update class #231 targeted is fully closed (0/90 with the lock); this is the correctly-scoped-out remainder.
Follow-up from cli#231 / cli#235 (config locking), characterized by the #235 adversarial verifier: 200 iterations of a sign-out (
deleteAuth) racing an in-flight token refresh (mutateAuth) -> 94 sign-out-won / 106 resurrected / 0 corrupt.Root cause (deeper than deleteAuth)
deleteAuthis lock-free, but making it locked only serializes the two ops - it does NOT stop resurrection. The actual cause: when a token refresh'smutateAuthre-reads a null (just-signed-out)auth.json,tryRefresh(src/lib/api.ts) falls back to the IN-MEMORY tokens and writes them back, resurrecting the session the user just ended.Correct fix
Two parts:
deleteAuththroughwithFileLock(make it async; the single callersetup.ts disconnectalready awaits) - removes the torn window, deterministic ordering.mutateAuth's refresh mutator should return null (skip the write) when the fresh disk read is null, instead of folding in-memory tokens. This is the part that actually kills resurrection.Severity
LOW: no corruption, atomic writes throughout, resurrected content is the user's own freshly-refreshed valid tokens (never stale/foreign), sub-ms window, and it requires an explicit
--disconnectracing a background refresh. The lost-update class #231 targeted is fully closed (0/90 with the lock); this is the correctly-scoped-out remainder.