fix(cliproxy): clear stale auth quota cache

- invalidate per-account quota cache entries after successful OAuth account registration

- add regression coverage for the Claude stale-reauth dashboard path
This commit is contained in:
Tam Nhu Tran
2026-04-14 15:11:34 -04:00
parent 9545499487
commit 769b54fb4f
2 changed files with 76 additions and 0 deletions
@@ -33,6 +33,7 @@ import {
} from '../../cliproxy/proxy-target-resolver';
import { fetchRemoteAuthStatus } from '../../cliproxy/remote-auth-fetcher';
import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes';
import { invalidateQuotaCache } from '../../cliproxy/quota-response-cache';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { tryKiroImport } from '../../cliproxy/auth/kiro-import';
import {
@@ -190,6 +191,13 @@ function shouldKeepWaitingForLocalToken(
);
}
function invalidateQuotaForRegisteredAccount(account: {
provider: CLIProxyProvider;
id: string;
}): void {
invalidateQuotaCache(account.provider, account.id);
}
function parseKiroMethod(raw: unknown): { method: KiroAuthMethod; invalid: boolean } {
if (raw === undefined || raw === null) {
return { method: normalizeKiroAuthMethod(), invalid: false };
@@ -1022,6 +1030,7 @@ router.get('/:provider/status', async (req: Request, res: Response): Promise<voi
} catch {
// Keep manual callback success path non-fatal when prefix repair cannot run.
}
invalidateQuotaForRegisteredAccount(account);
res.json({
status: 'ok',
account: {
@@ -1173,6 +1182,7 @@ router.post('/:provider/submit-callback', async (req: Request, res: Response): P
// Keep manual callback success path non-fatal when prefix repair cannot run.
}
}
invalidateQuotaForRegisteredAccount(account);
res.json({
success: true,
@@ -1205,6 +1215,8 @@ router.post('/:provider/submit-callback', async (req: Request, res: Response): P
return;
}
invalidateQuotaForRegisteredAccount(account);
res.json({
success: true,
account: {
@@ -6,6 +6,11 @@ import * as path from 'path';
import * as http from 'http';
import type { Server } from 'http';
import cliproxyAuthRoutes from '../../../src/web-server/routes/cliproxy-auth-routes';
import {
clearQuotaCache,
getCachedQuota,
setCachedQuota,
} from '../../../src/cliproxy/quota-response-cache';
import { restoreFetch, mockFetch } from '../../mocks';
describe('cliproxy-auth-routes manual callback nickname persistence', () => {
@@ -49,6 +54,7 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
afterEach(() => {
restoreFetch();
clearQuotaCache();
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
@@ -564,4 +570,62 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
expect(registry.providers.codex.accounts['new@example.com']?.email).toBe('new@example.com');
});
it('clears stale Claude quota cache after polling auth success', async () => {
mockFetch([
{
url: /\/v0\/management\/anthropic-auth-url\?is_webui=true$/,
response: {
auth_url: 'https://auth.example.com/authorize?state=state-claude-cache-clear',
state: 'state-claude-cache-clear',
},
},
{
url: /\/v0\/management\/get-auth-status\?state=state-claude-cache-clear$/,
response: { status: 'ok' },
},
]);
const startResponse = await postJson('/api/cliproxy/auth/claude/start-url', {});
expect(startResponse.status).toBe(200);
const accountId = 'claude-team@example.com';
setCachedQuota('claude', accountId, {
success: false,
error: 'Authentication required for policy limits',
needsReauth: true,
});
expect(getCachedQuota('claude', accountId)).not.toBeNull();
const tokenDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(tokenDir, { recursive: true });
fs.writeFileSync(
path.join(tokenDir, 'claude-claude-team@example.com.json'),
JSON.stringify({
type: 'claude',
email: accountId,
access_token: 'fresh-token',
refresh_token: 'refresh-token',
expired: '2099-01-01T00:00:00.000Z',
}),
'utf8'
);
const statusResponse = await getJson(
'/api/cliproxy/auth/claude/status?state=state-claude-cache-clear'
);
expect(statusResponse.status).toBe(200);
expect(statusResponse.body).toEqual({
status: 'ok',
account: {
id: accountId,
email: accountId,
nickname: 'claude-team',
provider: 'claude',
isDefault: true,
},
});
expect(getCachedQuota('claude', accountId)).toBeNull();
});
});