Repository navigation
fix(v2): keep refresh dedup alive across the host persistence gap - #92
Merged
Merged
Conversation
The V2 refresh handler deleted its dedup entry in a .finally attached to the broker promise, so the entry vanished when the broker responded -- strictly before refresh() resolved and before the OpenCode host persisted the rotated credential. A second concurrent resolve in that window re-read the still-stored old refresh token, missed the map, and re-sent the single-use token, which the broker rejects with 401 and surfaces as a transient tool failure. V1 does not have this race because it persists to its own store before its delete runs; V2 cannot, the host owns persistence, so the entry must outlive the settle. Delete on failure only (.catch with rethrow); successful entries stay cached under the key of the consumed old token so every late caller converges on the rotated result. Ceiling: one entry per rotation per account for the process lifetime, LRU-cap it if that ever matters. The dedup key used the raw brokerBaseUrl, but makeBrokerRequest() strips a trailing slash before hitting /refresh, so "https://host/broker" and "https://host/broker/" are the same endpoint with two different keys -- two Locations with these alias forms sent the credential twice. Export the exact replace(/\/$/, "") normalization from broker.ts and use it in both the request URL and the dedup key so aliases collapse to one entry. new URL().href semantics deliberately avoided: it keeps the trailing slash and would re-split the aliases. Fixes #90, fixes #91.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two bugs from the #87 final auth review, one fix site in the V2 refresh handler:
.finallydeleted the dedup entry when the broker responded — beforerefresh()resolved and before the OpenCode host persisted the rotated credential. A second resolve in that window re-read the still-stored old single-use token, missed the map, re-sent it, and got a 401 (transient tool failure). Now the entry is deleted on failure only; successful entries stay cached under the key of the consumed old token, so every late caller converges on the rotated result. Mirrors V1, which persists to its own store before its delete runs — V2 can't, the host owns persistence. Ceiling: one entry per rotation per account for the process lifetime (ponytail:comment marks the LRU upgrade path).brokerBaseUrl, whilemakeBrokerRequest()strips one trailing slash, sohttps://host/brokerandhttps://host/broker/were the same endpoint with two keys — two aliased Locations sent the credential twice. The exactreplace(/\/$/, "")normalization is now exported frombroker.tsand shared by the request URL and the key.new URL().hrefsemantics deliberately avoided (keeps the trailing slash).Evidence
2 fail / Ran 9 tests— the persistence-gap test fails with the mock broker's 401 (refresh token already used), the alias test sees two requests foralias-A.After:
bun run test test/v2-server.test.ts→9 pass / 0 fail; full suitebun run test→222 pass / 0 failacross 18 files;bun run lintandbun run typecheckclean.test/v2-server.test.ts:a second resolve in the host persistence gap converges on the completed refresh— first refresh fully resolves (broker response landed, nothing persisted), second resolve with the same old credential: exactly one broker request, both converge ongap-new-access/gap-new-refresh.failed refreshes release waiters, drop the pending entry, and retry hits the broker again— both waiters get the rejection, one broker call, retry after failure reaches the broker.trailing-slash broker aliases share one refresh; distinct brokers and tokens stay isolated—…/broker+…/broker/share one request; different origin and different path (…/broker2) and different token stay isolated..changeset/fix-v2-refresh-dedup.md(patch).Fixes #90, fixes #91.
Merge Danger
Door: two-way
Revert the commit and the old behavior returns; no data or schema changes.
Blast Radius: V2-only OAuth refresh path. V1 (
src/server/tools.ts) untouched. Worst case if the retention logic were wrong: a stale cached success served for a token the broker already rotated — bounded by process lifetime and per-old-token keys.