Repository navigation
Fix/claude refresh contract #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,27 @@ export async function refreshKimiCodingToken(refreshToken, log, proxyConfig = nu | |
| */ | ||
| export async function refreshClaudeOAuthToken(refreshToken, log, proxyConfig = null) { | ||
| try { | ||
| 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) | ||
| ); | ||
|
Comment on lines
+252
to
+256
|
||
|
|
||
| const response = await runWithProxyContext(proxyConfig, () => | ||
| fetch(OAUTH_ENDPOINTS.anthropic.token, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/x-www-form-urlencoded", | ||
| Accept: "application/json", | ||
| "anthropic-beta": "oauth-2025-04-20", | ||
| }, | ||
| headers, | ||
| body: params.toString(), | ||
| }) | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 OmniRoute Claude refresh contract and emits redacted request details", async () => { | ||||||||||
| const log = createLog(); | ||||||||||
| const calls = []; | ||||||||||
|
|
||||||||||
|
|
@@ -310,6 +310,22 @@ test("refreshClaudeOAuthToken matches upstream Claude refresh contract", async ( | |||||||||
| 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, | ||||||||||
| }); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test("refreshClaudeOAuthToken returns null on invalid Claude refresh contract rejection", async () => { | ||||||||||
|
|
@@ -338,7 +354,7 @@ test("refreshClaudeOAuthToken returns null on invalid Claude refresh contract re | |||||||||
| assert.equal(calls[0].options.headers["anthropic-beta"], "oauth-2025-04-20"); | ||||||||||
| assert.equal( | ||||||||||
| log.entries.some((entry) => entry.level === "warn"), | ||||||||||
|
||||||||||
| log.entries.some((entry) => entry.level === "warn"), | |
| log.entries.some( | |
| (entry) => entry.level === "warn" && entry.message === "Claude OAuth refresh request details" | |
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasProxyConfigis populated with the fullproxyConfigobject (passed as thehasProxyConfigargument). Proxy config objects can includeusername/password, so this risks leaking proxy credentials into logs and also doesn’t match the field name (implies a boolean). Consider logging a boolean (e.g.!!proxyConfig) or a redacted proxy label (host:port only) instead of the raw object.