diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index 0b2bdf01..eece3645 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -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 { @@ -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(); + }); });