mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 12:16:59 +00:00
fix(quota): reject tier-lock for non-managed providers
Only managed-quota providers (agy/claude/codex/gemini/ghcp) enforce tier_lock; validate the provider against that set so locking a non-managed provider returns 400 instead of persisting a silently-unenforced config entry.
This commit is contained in:
@@ -44,6 +44,10 @@ import {
|
||||
mutateConfig,
|
||||
} from '../../config/config-loader-facade';
|
||||
import type { AccountTier } from '../../cliproxy/accounts/types';
|
||||
import {
|
||||
isManagedQuotaProvider,
|
||||
MANAGED_QUOTA_PROVIDERS,
|
||||
} from '../../cliproxy/quota/quota-manager';
|
||||
|
||||
/** Valid account tier values for tier-lock validation */
|
||||
const VALID_ACCOUNT_TIERS: ReadonlySet<string> = new Set<AccountTier>([
|
||||
@@ -669,6 +673,18 @@ router.post('/tier-lock', (req: Request, res: Response): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fix #8: tier_lock is only enforced by quota-manager for managed-quota providers.
|
||||
// Locking a non-managed provider persists a silently-unenforced entry.
|
||||
// Reject with 400 to avoid misleading the caller.
|
||||
if (!isManagedQuotaProvider(provider)) {
|
||||
res.status(400).json({
|
||||
error:
|
||||
`Provider "${provider}" does not support tier-lock. ` +
|
||||
`Only managed-quota providers support it: ${[...MANAGED_QUOTA_PROVIDERS].join(', ')}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// tier must be explicitly present in body (key exists), and must be string or null
|
||||
if (!('tier' in req.body)) {
|
||||
res.status(400).json({ error: 'Missing required field: tier (string | null)' });
|
||||
|
||||
@@ -164,6 +164,41 @@ describe('POST /api/accounts/tier-lock', () => {
|
||||
expect(body.error).toMatch(/tier/i);
|
||||
});
|
||||
|
||||
it('rejects a non-managed provider with 400 (fix #8)', async () => {
|
||||
// Providers like 'kiro' or unknown CLIProxy providers accept isCLIProxyProvider()
|
||||
// but quota-manager does not enforce tier_lock for them. Persisting a lock entry
|
||||
// would silently have no effect, misleading the caller. Must 400.
|
||||
// Note: 'kiro' passes isCLIProxyProvider but is NOT in MANAGED_QUOTA_PROVIDERS.
|
||||
// We test with a provider that is valid (passes CLIProxy check) but not managed.
|
||||
// In practice this means any provider added to CLIProxy that is not in
|
||||
// MANAGED_QUOTA_PROVIDERS = ['agy', 'claude', 'codex', 'gemini', 'ghcp'].
|
||||
// We use a string that is a known CLIProxy provider but not managed.
|
||||
// Since the set of CLIProxy providers is dynamic we test the error message content.
|
||||
const res = await postJson(baseUrl, '/api/accounts/tier-lock', {
|
||||
provider: 'kiro',
|
||||
tier: 'pro',
|
||||
});
|
||||
// If kiro is a CLIProxy provider but not managed: expect 400
|
||||
// If kiro is not a CLIProxy provider at all: also 400 (from isCLIProxyProvider check)
|
||||
expect(res.status).toBe(400);
|
||||
const body = (await res.json()) as { error: string };
|
||||
expect(body.error).toMatch(/provider/i);
|
||||
});
|
||||
|
||||
it('accepts all five managed-quota providers (agy, claude, codex, gemini, ghcp)', async () => {
|
||||
const managedProviders = ['agy', 'claude', 'codex', 'gemini', 'ghcp'];
|
||||
for (const provider of managedProviders) {
|
||||
const res = await postJson(baseUrl, '/api/accounts/tier-lock', {
|
||||
provider,
|
||||
tier: 'pro',
|
||||
});
|
||||
// All managed providers should succeed (200)
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { provider: string; tier_lock: string | null };
|
||||
expect(body.provider).toBe(provider);
|
||||
}
|
||||
});
|
||||
|
||||
it('persists tier_lock as per-provider map in the config', async () => {
|
||||
await postJson(baseUrl, '/api/accounts/tier-lock', { provider: 'agy', tier: 'pro' });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user