fix(FN-000): restore Claude usage dashboard after Anthropic OAuth changes

Anthropic now requires `anthropic-beta: oauth-2025-04-20` on /api/oauth/usage
for OAuth-scoped tokens; without it the endpoint returns 401 "OAuth
authentication is currently not supported" and the dashboard falls back to
a PTY-based CLI parser that times out at 75s. Mirrors the header the Claude
CLI (2.1.114) sends from `claude /usage`, and also aligns the refresh call
with the CLI's JSON body + `scope` field so token refresh keeps working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-18 20:53:55 -07:00
parent 342dd48350
commit 806f8ab042
2 changed files with 52 additions and 18 deletions

View File

@@ -446,7 +446,7 @@ describe("usage", () => {
expect(claude.status).toBe("error");
});
it("does not send anthropic-beta header in requests", async () => {
it("sends anthropic-beta=oauth-2025-04-20 header so the OAuth usage endpoint authorizes the request", async () => {
const mockResponse = {
five_hour: { utilization: 10.0 },
};
@@ -491,8 +491,11 @@ describe("usage", () => {
await fetchAllProviderUsage();
// Verify no anthropic-beta header is sent
expect(capturedHeaders).not.toHaveProperty("anthropic-beta");
// Anthropic now requires the `anthropic-beta: oauth-2025-04-20` header on
// /api/oauth/usage for OAuth-scoped tokens; without it the endpoint
// replies with 401 "OAuth authentication is currently not supported".
// The value mirrors what the Claude CLI sends from `claude /usage`.
expect(capturedHeaders["anthropic-beta"]).toBe("oauth-2025-04-20");
});
it("retries on 429 and succeeds after transient rate limit", async () => {
@@ -1206,19 +1209,20 @@ describe("usage", () => {
await fetchAllProviderUsage();
// Verify the refresh request (first call)
// Verify the refresh request (first call). Body is JSON with a `scope`
// field — matches what the Claude CLI sends; form-urlencoded bodies or
// missing scope cause Anthropic to reject the refresh.
const refreshOpts = capturedOptions[0];
expect(refreshOpts.hostname).toBe("platform.claude.com");
expect(refreshOpts.path).toBe("/v1/oauth/token");
expect(refreshOpts.headers["content-type"]).toBe("application/x-www-form-urlencoded");
expect(refreshOpts.headers["content-type"]).toBe("application/json");
// Verify body contains required parameters
expect(capturedBodies.length).toBeGreaterThanOrEqual(1);
const body = capturedBodies[0];
const params = new URLSearchParams(body);
expect(params.get("grant_type")).toBe("refresh_token");
expect(params.get("refresh_token")).toBe("refresh-token-456");
expect(params.get("client_id")).toBe("9d1c250a-e61b-44d9-88ed-5944d1962f5e");
const body = JSON.parse(capturedBodies[0]);
expect(body.grant_type).toBe("refresh_token");
expect(body.refresh_token).toBe("refresh-token-456");
expect(body.client_id).toBe("9d1c250a-e61b-44d9-88ed-5944d1962f5e");
expect(body.scope).toBe("user:profile");
});
it("falls back to CLI when refresh fails for expired token", async () => {