From 43cd5ed2882230e6f599fbd62bab57ac3c97e803 Mon Sep 17 00:00:00 2001 From: i1hwan Date: Sun, 12 Apr 2026 06:21:05 +0900 Subject: [PATCH 1/2] fix(auth): trim Claude refresh request shape Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- open-sse/services/tokenRefresh.ts | 9 ++++----- tests/unit/token-refresh-service.test.mjs | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/open-sse/services/tokenRefresh.ts b/open-sse/services/tokenRefresh.ts index ff1f5ee6cd90..27048d4a3188 100755 --- a/open-sse/services/tokenRefresh.ts +++ b/open-sse/services/tokenRefresh.ts @@ -222,21 +222,20 @@ export async function refreshKimiCodingToken(refreshToken, log, proxyConfig = nu */ export async function refreshClaudeOAuthToken(refreshToken, log, proxyConfig = null) { try { - const params = new URLSearchParams({ + const payload = { grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS.claude.clientId, - }); + }; const response = await runWithProxyContext(proxyConfig, () => fetch(OAUTH_ENDPOINTS.anthropic.token, { method: "POST", headers: { - "Content-Type": "application/x-www-form-urlencoded", + "Content-Type": "application/json", Accept: "application/json", - "anthropic-beta": "oauth-2025-04-20", }, - body: params.toString(), + body: JSON.stringify(payload), }) ); diff --git a/tests/unit/token-refresh-service.test.mjs b/tests/unit/token-refresh-service.test.mjs index 000736dbc0f3..91972a2d728c 100644 --- a/tests/unit/token-refresh-service.test.mjs +++ b/tests/unit/token-refresh-service.test.mjs @@ -279,7 +279,7 @@ test("refreshKimiCodingToken adds provider-specific headers and fields", async ( assert.match(bodyToString(calls[0].options.body), /grant_type=refresh_token/); }); -test("refreshClaudeOAuthToken matches upstream Claude refresh contract", async () => { +test("refreshClaudeOAuthToken matches Claude token exchange request shape without extra fields", async () => { const log = createLog(); const calls = []; @@ -303,13 +303,14 @@ test("refreshClaudeOAuthToken matches upstream Claude refresh contract", async ( ); assert.equal(calls[0].url, OAUTH_ENDPOINTS.anthropic.token); - assert.equal(calls[0].options.headers["Content-Type"], "application/x-www-form-urlencoded"); - assert.equal(calls[0].options.headers["anthropic-beta"], "oauth-2025-04-20"); + assert.equal(calls[0].options.headers["Content-Type"], "application/json"); + assert.equal(calls[0].options.headers["anthropic-beta"], undefined); assert.equal(calls[0].options.headers["User-Agent"], undefined); - assert.equal( - bodyToString(calls[0].options.body), - `grant_type=refresh_token&refresh_token=claude-refresh&client_id=${encodeURIComponent(PROVIDERS.claude.clientId)}` - ); + assert.deepEqual(JSON.parse(bodyToString(calls[0].options.body)), { + grant_type: "refresh_token", + refresh_token: "claude-refresh", + client_id: PROVIDERS.claude.clientId, + }); }); test("refreshClaudeOAuthToken returns null on invalid Claude refresh contract rejection", async () => { @@ -334,8 +335,8 @@ test("refreshClaudeOAuthToken returns null on invalid Claude refresh contract re ); assert.equal(calls.length, 1); - assert.equal(calls[0].options.headers["Content-Type"], "application/x-www-form-urlencoded"); - assert.equal(calls[0].options.headers["anthropic-beta"], "oauth-2025-04-20"); + assert.equal(calls[0].options.headers["Content-Type"], "application/json"); + assert.equal(calls[0].options.headers["anthropic-beta"], undefined); assert.equal( log.entries.some((entry) => entry.level === "warn"), false From aeffa366b4ffc5509017246e01e608488e189976 Mon Sep 17 00:00:00 2001 From: i1hwan Date: Sun, 12 Apr 2026 06:30:02 +0900 Subject: [PATCH 2/2] fix(auth): log Claude refresh request details Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- open-sse/services/tokenRefresh.ts | 38 ++++++++++++++++++----- tests/unit/token-refresh-service.test.mjs | 35 +++++++++++++++------ 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/open-sse/services/tokenRefresh.ts b/open-sse/services/tokenRefresh.ts index 27048d4a3188..f10b6fdc80c1 100755 --- a/open-sse/services/tokenRefresh.ts +++ b/open-sse/services/tokenRefresh.ts @@ -16,6 +16,22 @@ function getRefreshCacheKey(provider, refreshToken) { return `${provider}:${tokenHash}`; } +function buildClaudeRefreshRequestMeta(refreshToken, headers, body, hasProxyConfig) { + return { + method: "POST", + url: OAUTH_ENDPOINTS.anthropic.token, + contentType: headers["Content-Type"], + accept: headers.Accept, + anthropicBeta: headers["anthropic-beta"] || null, + bodyFormat: + headers["Content-Type"] === "application/x-www-form-urlencoded" ? "form-urlencoded" : "json", + bodyKeys: body instanceof URLSearchParams ? Array.from(body.keys()) : Object.keys(body || {}), + clientId: PROVIDERS.claude.clientId || null, + refreshTokenLength: typeof refreshToken === "string" ? refreshToken.length : 0, + hasProxyConfig: hasProxyConfig, + }; +} + /** * Refresh OAuth access token using refresh token */ @@ -222,20 +238,28 @@ export async function refreshKimiCodingToken(refreshToken, log, proxyConfig = nu */ export async function refreshClaudeOAuthToken(refreshToken, log, proxyConfig = null) { try { - const payload = { + const headers = { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + "anthropic-beta": "oauth-2025-04-20", + }; + const params = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS.claude.clientId, - }; + }); + + log?.warn?.( + "TOKEN_REFRESH", + "Claude OAuth refresh request details", + buildClaudeRefreshRequestMeta(refreshToken, headers, params, proxyConfig) + ); const response = await runWithProxyContext(proxyConfig, () => fetch(OAUTH_ENDPOINTS.anthropic.token, { method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(payload), + headers, + body: params.toString(), }) ); diff --git a/tests/unit/token-refresh-service.test.mjs b/tests/unit/token-refresh-service.test.mjs index 91972a2d728c..8e4ea939f632 100644 --- a/tests/unit/token-refresh-service.test.mjs +++ b/tests/unit/token-refresh-service.test.mjs @@ -279,7 +279,7 @@ test("refreshKimiCodingToken adds provider-specific headers and fields", async ( assert.match(bodyToString(calls[0].options.body), /grant_type=refresh_token/); }); -test("refreshClaudeOAuthToken matches Claude token exchange request shape without extra fields", async () => { +test("refreshClaudeOAuthToken matches OmniRoute Claude refresh contract and emits redacted request details", async () => { const log = createLog(); const calls = []; @@ -303,13 +303,28 @@ test("refreshClaudeOAuthToken matches Claude token exchange request shape withou ); assert.equal(calls[0].url, OAUTH_ENDPOINTS.anthropic.token); - assert.equal(calls[0].options.headers["Content-Type"], "application/json"); - assert.equal(calls[0].options.headers["anthropic-beta"], undefined); + assert.equal(calls[0].options.headers["Content-Type"], "application/x-www-form-urlencoded"); + assert.equal(calls[0].options.headers["anthropic-beta"], "oauth-2025-04-20"); assert.equal(calls[0].options.headers["User-Agent"], undefined); - assert.deepEqual(JSON.parse(bodyToString(calls[0].options.body)), { - grant_type: "refresh_token", - refresh_token: "claude-refresh", - client_id: PROVIDERS.claude.clientId, + assert.equal( + bodyToString(calls[0].options.body), + `grant_type=refresh_token&refresh_token=claude-refresh&client_id=${encodeURIComponent(PROVIDERS.claude.clientId)}` + ); + + const detailLog = log.entries.find( + (entry) => entry.level === "warn" && entry.message === "Claude OAuth refresh request details" + ); + assert.deepEqual(detailLog?.meta, { + method: "POST", + url: OAUTH_ENDPOINTS.anthropic.token, + contentType: "application/x-www-form-urlencoded", + accept: "application/json", + anthropicBeta: "oauth-2025-04-20", + bodyFormat: "form-urlencoded", + bodyKeys: ["grant_type", "refresh_token", "client_id"], + clientId: PROVIDERS.claude.clientId || null, + refreshTokenLength: "claude-refresh".length, + hasProxyConfig: null, }); }); @@ -335,11 +350,11 @@ test("refreshClaudeOAuthToken returns null on invalid Claude refresh contract re ); assert.equal(calls.length, 1); - assert.equal(calls[0].options.headers["Content-Type"], "application/json"); - assert.equal(calls[0].options.headers["anthropic-beta"], undefined); + assert.equal(calls[0].options.headers["Content-Type"], "application/x-www-form-urlencoded"); + assert.equal(calls[0].options.headers["anthropic-beta"], "oauth-2025-04-20"); assert.equal( log.entries.some((entry) => entry.level === "warn"), - false + true ); assert.equal( log.entries.some(