Skip to content

Commit e37c134

Browse files
committed
fix: address token refresh race condition and missing updates
- Add tokenRefreshPromise caching to prevent concurrent refresh calls - Update auth.expires after token refresh (was missing) - Add user:sessions:claude_code to OAuth scope (matches Claude Code CLI) - Add AskUserQuestion to response tool name regex - Add TextDecoder flush to handle incomplete multi-byte sequences
1 parent e9bf805 commit e37c134

File tree

1 file changed

+32
-13
lines changed

1 file changed

+32
-13
lines changed

index.mjs

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const TOOL_NAME_CACHE = new Map();
4343
const TOOL_PREFIX_REGEX = /^(?:oc_|mcp_)/i;
4444

4545
let cachedMetadataUserIdPromise;
46+
let tokenRefreshPromise = null;
4647

4748
// ============================================================================
4849
// Debug Logging
@@ -283,7 +284,7 @@ function replaceToolNamesInText(text) {
283284
let output = text.replace(/"name"\s*:\s*"(?:oc_|mcp_)([^"]+)"/g, '"name": "$1"');
284285

285286
output = output.replace(
286-
/"name"\s*:\s*"(Bash|Read|Edit|Write|Task|Glob|Grep|WebFetch|WebSearch|TodoWrite)"/g,
287+
/"name"\s*:\s*"(Bash|Read|Edit|Write|Task|Glob|Grep|WebFetch|WebSearch|TodoWrite|AskUserQuestion)"/g,
287288
(_, name) => `"name": "${normalizeToolNameForOpenCode(name)}"`,
288289
);
289290

@@ -382,6 +383,11 @@ function createTransformedResponse(response) {
382383
async pull(controller) {
383384
const { done, value } = await reader.read();
384385
if (done) {
386+
// Flush any remaining bytes from the decoder
387+
const flushed = decoder.decode(new Uint8Array(), { stream: false });
388+
if (flushed) {
389+
buffer += flushed;
390+
}
385391
// Process any remaining buffered content
386392
if (buffer.length > 0) {
387393
controller.enqueue(encoder.encode(replaceToolNamesInText(buffer)));
@@ -642,7 +648,7 @@ async function authorize(mode) {
642648
url.searchParams.set("client_id", CLIENT_ID);
643649
url.searchParams.set("response_type", "code");
644650
url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback");
645-
url.searchParams.set("scope", "org:create_api_key user:profile user:inference");
651+
url.searchParams.set("scope", "org:create_api_key user:profile user:inference user:sessions:claude_code");
646652
url.searchParams.set("code_challenge", pkce.challenge);
647653
url.searchParams.set("code_challenge_method", "S256");
648654
url.searchParams.set("state", pkce.verifier);
@@ -713,17 +719,30 @@ export async function AnthropicAuthPlugin({ client }) {
713719
const baseFetch = getBaseFetch();
714720

715721
if (!auth.access || auth.expires < Date.now()) {
716-
const json = await refreshOAuthToken(auth, baseFetch);
717-
await client.auth.set({
718-
path: { id: "anthropic" },
719-
body: {
720-
type: "oauth",
721-
refresh: json.refresh_token,
722-
access: json.access_token,
723-
expires: Date.now() + json.expires_in * 1000,
724-
},
725-
});
726-
auth.access = json.access_token;
722+
// Prevent race condition: cache the refresh promise
723+
if (!tokenRefreshPromise) {
724+
tokenRefreshPromise = (async () => {
725+
try {
726+
const json = await refreshOAuthToken(auth, baseFetch);
727+
const newExpires = Date.now() + json.expires_in * 1000;
728+
await client.auth.set({
729+
path: { id: "anthropic" },
730+
body: {
731+
type: "oauth",
732+
refresh: json.refresh_token,
733+
access: json.access_token,
734+
expires: newExpires,
735+
},
736+
});
737+
auth.access = json.access_token;
738+
auth.expires = newExpires;
739+
return json;
740+
} finally {
741+
tokenRefreshPromise = null;
742+
}
743+
})();
744+
}
745+
await tokenRefreshPromise;
727746
}
728747

729748
return handleAnthropicRequest(input, init, auth, baseFetch);

0 commit comments

Comments
 (0)