mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-09 02:17:03 +00:00
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:
@@ -33,6 +33,7 @@ import {
|
|||||||
} from '../../cliproxy/proxy-target-resolver';
|
} from '../../cliproxy/proxy-target-resolver';
|
||||||
import { fetchRemoteAuthStatus } from '../../cliproxy/remote-auth-fetcher';
|
import { fetchRemoteAuthStatus } from '../../cliproxy/remote-auth-fetcher';
|
||||||
import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes';
|
import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes';
|
||||||
|
import { invalidateQuotaCache } from '../../cliproxy/quota-response-cache';
|
||||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
import { tryKiroImport } from '../../cliproxy/auth/kiro-import';
|
import { tryKiroImport } from '../../cliproxy/auth/kiro-import';
|
||||||
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 } {
|
function parseKiroMethod(raw: unknown): { method: KiroAuthMethod; invalid: boolean } {
|
||||||
if (raw === undefined || raw === null) {
|
if (raw === undefined || raw === null) {
|
||||||
return { method: normalizeKiroAuthMethod(), invalid: false };
|
return { method: normalizeKiroAuthMethod(), invalid: false };
|
||||||
@@ -1022,6 +1030,7 @@ router.get('/:provider/status', async (req: Request, res: Response): Promise<voi
|
|||||||
} catch {
|
} catch {
|
||||||
// Keep manual callback success path non-fatal when prefix repair cannot run.
|
// Keep manual callback success path non-fatal when prefix repair cannot run.
|
||||||
}
|
}
|
||||||
|
invalidateQuotaForRegisteredAccount(account);
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
account: {
|
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.
|
// Keep manual callback success path non-fatal when prefix repair cannot run.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
invalidateQuotaForRegisteredAccount(account);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -1205,6 +1215,8 @@ router.post('/:provider/submit-callback', async (req: Request, res: Response): P
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidateQuotaForRegisteredAccount(account);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
account: {
|
account: {
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import * as path from 'path';
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import type { Server } from 'http';
|
import type { Server } from 'http';
|
||||||
import cliproxyAuthRoutes from '../../../src/web-server/routes/cliproxy-auth-routes';
|
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';
|
import { restoreFetch, mockFetch } from '../../mocks';
|
||||||
|
|
||||||
describe('cliproxy-auth-routes manual callback nickname persistence', () => {
|
describe('cliproxy-auth-routes manual callback nickname persistence', () => {
|
||||||
@@ -49,6 +54,7 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
restoreFetch();
|
restoreFetch();
|
||||||
|
clearQuotaCache();
|
||||||
|
|
||||||
if (originalCcsHome === undefined) {
|
if (originalCcsHome === undefined) {
|
||||||
delete process.env.CCS_HOME;
|
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');
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user