From 457412277b1311decb660620ecdd0cdd78a90e1c Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 30 May 2026 14:53:53 -0400 Subject: [PATCH] fix(cliproxy): sanitize Codex quota labels --- .../__tests__/quota-fetcher-codex.test.ts | 38 +++++++++++++++++++ src/cliproxy/quota/quota-fetcher-codex.ts | 11 ++---- src/cliproxy/quota/quota-label-sanitizer.ts | 25 ++++++++++++ src/commands/cliproxy/quota-subcommand.ts | 9 +++-- .../cliproxy-quota-subcommand.test.ts | 32 ++++++++++++++++ 5 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 src/cliproxy/quota/quota-label-sanitizer.ts diff --git a/src/cliproxy/quota/__tests__/quota-fetcher-codex.test.ts b/src/cliproxy/quota/__tests__/quota-fetcher-codex.test.ts index 7a913713..d51e3af9 100644 --- a/src/cliproxy/quota/__tests__/quota-fetcher-codex.test.ts +++ b/src/cliproxy/quota/__tests__/quota-fetcher-codex.test.ts @@ -382,6 +382,44 @@ describe('Codex Quota Fetcher', () => { expect(windows[0].featureLabel).toBe('Custom-Feature'); expect(windows[0].usedPercent).toBe(50); }); + + it('should remove terminal control characters from additional limit labels', () => { + const response = { + additional_rate_limits: [ + { + limit_name: '\u001b[2JGPT-5.3-Codex-Spark\u001b]52;c;payload\u0007', + rate_limit: { + primary_window: { used_percent: 25, reset_after_seconds: 3600 }, + }, + }, + ], + }; + + const windows = buildCodexQuotaWindows(response); + + expect(windows).toHaveLength(1); + expect(windows[0].featureLabel).toBe('GPT-5.3-Codex-Spark'); + expect(windows[0].label).toBe('GPT-5.3-Codex-Spark (Primary)'); + }); + + it('should bound additional limit labels before storing them', () => { + const response = { + additional_rate_limits: [ + { + limit_name: `Feature-${'x'.repeat(120)}`, + rate_limit: { + primary_window: { used_percent: 25, reset_after_seconds: 3600 }, + }, + }, + ], + }; + + const windows = buildCodexQuotaWindows(response); + + expect(windows).toHaveLength(1); + expect(windows[0].featureLabel).toHaveLength(80); + expect(windows[0].label).toBe(`${windows[0].featureLabel} (Primary)`); + }); }); describe('buildCodexCoreUsageSummary', () => { diff --git a/src/cliproxy/quota/quota-fetcher-codex.ts b/src/cliproxy/quota/quota-fetcher-codex.ts index 78ab225b..c9d66a94 100644 --- a/src/cliproxy/quota/quota-fetcher-codex.ts +++ b/src/cliproxy/quota/quota-fetcher-codex.ts @@ -11,6 +11,7 @@ import { getAuthDir } from '../config/config-generator'; import { getAccount, getProviderAccounts, getPausedDir } from '../accounts/account-manager'; import { sanitizeEmail, isTokenExpired } from '../auth/auth-utils'; import type { CodexQuotaResult, CodexQuotaWindow, CodexCoreUsageSummary } from './quota-types'; +import { sanitizeCodexFeatureLabel } from './quota-label-sanitizer'; import { extractCanonicalEmailFromAccountId } from '../accounts/email-account-identity'; /** ChatGPT backend API base URL */ @@ -58,8 +59,8 @@ interface CodexRateLimitWindow { * Each entry surfaces its own primary/secondary windows under a feature-specific limit name. */ interface CodexAdditionalRateLimit { - limit_name?: string; - limitName?: string; + limit_name?: unknown; + limitName?: unknown; metered_feature?: string; meteredFeature?: string; rate_limit?: CodexRateLimitWindow; @@ -380,11 +381,7 @@ function buildCodexQuotaWindows(payload: CodexUsageResponse): CodexQuotaWindow[] const entryRateLimit = entry.rate_limit || entry.rateLimit; if (!entryRateLimit) continue; - const rawFeatureLabel = entry.limit_name ?? entry.limitName; - const featureLabel = - typeof rawFeatureLabel === 'string' && rawFeatureLabel.trim().length > 0 - ? rawFeatureLabel.trim() - : 'Additional'; + const featureLabel = sanitizeCodexFeatureLabel(entry.limit_name ?? entry.limitName); addWindow( `${featureLabel} (Primary)`, entryRateLimit.primary_window || entryRateLimit.primaryWindow, diff --git a/src/cliproxy/quota/quota-label-sanitizer.ts b/src/cliproxy/quota/quota-label-sanitizer.ts new file mode 100644 index 00000000..438527ea --- /dev/null +++ b/src/cliproxy/quota/quota-label-sanitizer.ts @@ -0,0 +1,25 @@ +const CODEX_FEATURE_LABEL_FALLBACK = 'Additional'; +const CODEX_FEATURE_LABEL_MAX_LENGTH = 80; +const TERMINAL_ESCAPE_SEQUENCE_REGEX = + /\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u001b\[[0-?]*[ -/]*[@-~]/g; +const TERMINAL_CONTROL_CHARS_REGEX = /[\u0000-\u001f\u007f-\u009f]/g; + +/** + * Sanitize upstream Codex feature labels before storing or rendering them. + * + * The quota API is not schema-validated at runtime, so additional-rate-limit + * labels must be constrained to safe, printable strings before they reach the + * terminal. + */ +export function sanitizeCodexFeatureLabel(value: unknown): string { + if (typeof value !== 'string') return CODEX_FEATURE_LABEL_FALLBACK; + + const sanitized = value + .replace(TERMINAL_ESCAPE_SEQUENCE_REGEX, '') + .replace(TERMINAL_CONTROL_CHARS_REGEX, '') + .trim() + .slice(0, CODEX_FEATURE_LABEL_MAX_LENGTH) + .trimEnd(); + + return sanitized.length > 0 ? sanitized : CODEX_FEATURE_LABEL_FALLBACK; +} diff --git a/src/commands/cliproxy/quota-subcommand.ts b/src/commands/cliproxy/quota-subcommand.ts index b53dd770..2efdbd05 100644 --- a/src/commands/cliproxy/quota-subcommand.ts +++ b/src/commands/cliproxy/quota-subcommand.ts @@ -18,6 +18,7 @@ import { } from '../../cliproxy/accounts/account-manager'; import { fetchAllProviderQuotas } from '../../cliproxy/quota/quota-fetcher'; import { fetchAllCodexQuotas } from '../../cliproxy/quota/quota-fetcher-codex'; +import { sanitizeCodexFeatureLabel } from '../../cliproxy/quota/quota-label-sanitizer'; import { fetchAllClaudeQuotas } from '../../cliproxy/quota/quota-fetcher-claude'; import { pickMostRestrictiveClaudeWeeklyWindow } from '../../cliproxy/quota/quota-fetcher-claude-normalizer'; import { fetchAllGeminiCliQuotas } from '../../cliproxy/quota/quota-fetcher-gemini-cli'; @@ -279,8 +280,8 @@ function inferCodeReviewCadence( * Strip a leading "GPT-X.Y-Codex-" prefix from a feature label and turn the * remainder into a Codex-prefixed display name. Other labels pass through unchanged. */ -function prettifyCodexFeatureLabel(featureLabel: string): string { - const trimmed = featureLabel.trim(); +function prettifyCodexFeatureLabel(featureLabel: unknown): string { + const trimmed = sanitizeCodexFeatureLabel(featureLabel); if (!trimmed) return 'Additional'; const stripped = trimmed.replace(/^GPT-[\d.]+-Codex-/i, ''); if (stripped !== trimmed && stripped.length > 0) { @@ -302,7 +303,7 @@ function getCodexWindowDisplayLabel( } if (window.category === 'additional') { - const pretty = prettifyCodexFeatureLabel(window.featureLabel || window.label || 'Additional'); + const pretty = prettifyCodexFeatureLabel(window.featureLabel ?? window.label); if (window.cadence === '5h') return `${pretty} (5h)`; if (window.cadence === 'weekly') return `${pretty} (weekly)`; return pretty; @@ -850,7 +851,9 @@ const QUOTA_PROVIDER_RUNTIME: Record { expect(resolveDisplayedTier('pro', 'unknown')).toBe('pro'); }); }); + +describe('cliproxy quota subcommand Codex label formatting', () => { + it('falls back for non-string cached Codex feature labels', async () => { + const { getCodexWindowDisplayLabel } = await loadQuotaCommandTestExports(); + + const label = getCodexWindowDisplayLabel({ + label: 'ignored', + resetAfterSeconds: 3600, + category: 'additional', + cadence: '5h', + featureLabel: { unexpected: true }, + } as never); + + expect(label).toBe('Additional (5h)'); + }); + + it('removes terminal control characters from cached Codex feature labels', async () => { + const { getCodexWindowDisplayLabel } = await loadQuotaCommandTestExports(); + + const label = getCodexWindowDisplayLabel({ + label: 'ignored', + resetAfterSeconds: 3600, + category: 'additional', + cadence: 'weekly', + featureLabel: '\u001b[2JGPT-5.3-Codex-Spark\u001b]52;c;payload\u0007', + }); + + expect(label).toBe('Codex Spark (weekly)'); + expect(label).not.toContain('\u001b'); + expect(label).not.toContain('\u0007'); + }); +});