mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
fix(cliproxy): split claude quota fetcher for maintainability gate
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Claude Quota Response Normalization Helpers
|
||||
*
|
||||
* Parses Anthropic policy limits payload into normalized windows and core usage summary.
|
||||
*/
|
||||
|
||||
import type { ClaudeCoreUsageSummary, ClaudeQuotaWindow } from './quota-types';
|
||||
import { clampPercent } from '../utils/percentage';
|
||||
|
||||
function asString(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function asBoolean(value: unknown): boolean | undefined {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'string') {
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function asNumber(value: unknown): number | null {
|
||||
if (typeof value === 'number' && isFinite(value)) return value;
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
return isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeTimestamp(value: unknown): string | null {
|
||||
const asNum = asNumber(value);
|
||||
if (asNum !== null) {
|
||||
const millis = asNum > 1e12 ? asNum : asNum * 1000;
|
||||
const date = new Date(millis);
|
||||
return isNaN(date.getTime()) ? null : date.toISOString();
|
||||
}
|
||||
|
||||
const str = asString(value);
|
||||
if (!str) return null;
|
||||
|
||||
// Numeric strings can be either epoch seconds or epoch milliseconds.
|
||||
if (/^\d+$/.test(str)) {
|
||||
const numeric = Number(str);
|
||||
if (isFinite(numeric)) {
|
||||
const millis = numeric > 1e12 ? numeric : numeric * 1000;
|
||||
const date = new Date(millis);
|
||||
return isNaN(date.getTime()) ? null : date.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(str);
|
||||
return isNaN(date.getTime()) ? null : date.toISOString();
|
||||
}
|
||||
|
||||
function getClaudeWindowLabel(rateLimitType: string): string {
|
||||
switch (rateLimitType) {
|
||||
case 'five_hour':
|
||||
return 'Session limit';
|
||||
case 'seven_day':
|
||||
return 'Weekly limit';
|
||||
case 'seven_day_opus':
|
||||
return 'Opus limit';
|
||||
case 'seven_day_sonnet':
|
||||
return 'Sonnet limit';
|
||||
case 'overage':
|
||||
return 'Extra usage';
|
||||
default:
|
||||
return rateLimitType || 'Unknown limit';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUtilization(raw: Record<string, unknown>): {
|
||||
utilization: number | null;
|
||||
usedPercent: number;
|
||||
remainingPercent: number;
|
||||
} {
|
||||
const utilizationRaw = asNumber(raw['utilization']);
|
||||
const usedPercentRaw = asNumber(raw['usedPercent'] ?? raw['used_percent']);
|
||||
const remainingPercentRaw = asNumber(raw['remainingPercent'] ?? raw['remaining_percent']);
|
||||
|
||||
if (utilizationRaw !== null) {
|
||||
const ratio = utilizationRaw <= 1 ? utilizationRaw : utilizationRaw / 100;
|
||||
const usedPercent = clampPercent(ratio * 100);
|
||||
return {
|
||||
utilization: ratio,
|
||||
usedPercent,
|
||||
remainingPercent: clampPercent(100 - usedPercent),
|
||||
};
|
||||
}
|
||||
|
||||
if (usedPercentRaw !== null) {
|
||||
const usedPercent = clampPercent(usedPercentRaw);
|
||||
return {
|
||||
utilization: usedPercent / 100,
|
||||
usedPercent,
|
||||
remainingPercent: clampPercent(100 - usedPercent),
|
||||
};
|
||||
}
|
||||
|
||||
if (remainingPercentRaw !== null) {
|
||||
const remainingPercent = clampPercent(remainingPercentRaw);
|
||||
const usedPercent = clampPercent(100 - remainingPercent);
|
||||
return {
|
||||
utilization: usedPercent / 100,
|
||||
usedPercent,
|
||||
remainingPercent,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
utilization: null,
|
||||
usedPercent: 0,
|
||||
remainingPercent: 100,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRateLimitType(value: unknown, fallbackKey?: string): string {
|
||||
const direct = asString(value);
|
||||
if (direct) return direct;
|
||||
if (fallbackKey) return fallbackKey;
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function toObject(value: unknown): Record<string, unknown> | null {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function normalizeRestriction(
|
||||
raw: Record<string, unknown>,
|
||||
fallbackKey?: string
|
||||
): ClaudeQuotaWindow | null {
|
||||
const rateLimitType = normalizeRateLimitType(
|
||||
raw['rateLimitType'] ?? raw['rate_limit_type'] ?? raw['claim'] ?? raw['claimAbbrev'],
|
||||
fallbackKey
|
||||
);
|
||||
if (!rateLimitType || rateLimitType === 'unknown') return null;
|
||||
|
||||
const status = asString(raw['status']) || 'unknown';
|
||||
const resetAt =
|
||||
normalizeTimestamp(raw['resetsAt'] ?? raw['resets_at'] ?? raw['resetAt'] ?? raw['reset_at']) ||
|
||||
null;
|
||||
const overageResetsAt =
|
||||
normalizeTimestamp(
|
||||
raw['overageResetsAt'] ??
|
||||
raw['overage_resets_at'] ??
|
||||
raw['overageResetAt'] ??
|
||||
raw['overage_reset_at']
|
||||
) || null;
|
||||
|
||||
const { utilization, usedPercent, remainingPercent } = normalizeUtilization(raw);
|
||||
|
||||
return {
|
||||
rateLimitType,
|
||||
label: getClaudeWindowLabel(rateLimitType),
|
||||
status,
|
||||
utilization,
|
||||
usedPercent,
|
||||
remainingPercent,
|
||||
resetAt,
|
||||
surpassedThreshold: asBoolean(raw['surpassedThreshold'] ?? raw['surpassed_threshold']),
|
||||
severity: asString(raw['severity']) || undefined,
|
||||
overageStatus: asString(raw['overageStatus'] ?? raw['overage_status']) || undefined,
|
||||
overageResetsAt,
|
||||
overageDisabledReason:
|
||||
asString(raw['overageDisabledReason'] ?? raw['overage_disabled_reason']) || undefined,
|
||||
isUsingOverage: asBoolean(raw['isUsingOverage'] ?? raw['is_using_overage']),
|
||||
hasExtraUsageEnabled: asBoolean(raw['hasExtraUsageEnabled'] ?? raw['has_extra_usage_enabled']),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw policy limits response into normalized windows.
|
||||
* Supports both array and object-map `restrictions` shapes.
|
||||
*/
|
||||
export function buildClaudeQuotaWindows(payload: Record<string, unknown>): ClaudeQuotaWindow[] {
|
||||
const rawRestrictions = payload['restrictions'];
|
||||
const windows: ClaudeQuotaWindow[] = [];
|
||||
|
||||
if (Array.isArray(rawRestrictions)) {
|
||||
for (const item of rawRestrictions) {
|
||||
const raw = toObject(item);
|
||||
if (!raw) continue;
|
||||
const window = normalizeRestriction(raw);
|
||||
if (window) windows.push(window);
|
||||
}
|
||||
} else if (toObject(rawRestrictions)) {
|
||||
for (const [key, value] of Object.entries(rawRestrictions as Record<string, unknown>)) {
|
||||
const raw = toObject(value);
|
||||
if (!raw) continue;
|
||||
const window = normalizeRestriction(raw, key);
|
||||
if (window) windows.push(window);
|
||||
}
|
||||
} else if (toObject(payload)) {
|
||||
// Some responses may contain a single restriction object directly.
|
||||
const direct = normalizeRestriction(payload);
|
||||
if (direct) windows.push(direct);
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
const unique: ClaudeQuotaWindow[] = [];
|
||||
for (const window of windows) {
|
||||
const key = `${window.rateLimitType}:${window.resetAt ?? ''}:${window.status}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
unique.push(window);
|
||||
}
|
||||
|
||||
return unique.sort((a, b) => a.rateLimitType.localeCompare(b.rateLimitType));
|
||||
}
|
||||
|
||||
function toEpochMs(iso: string | null): number | null {
|
||||
if (!iso) return null;
|
||||
const value = new Date(iso).getTime();
|
||||
return isNaN(value) ? null : value;
|
||||
}
|
||||
|
||||
function pickMostRestrictiveWeekly(windows: ClaudeQuotaWindow[]): ClaudeQuotaWindow | null {
|
||||
if (windows.length === 0) return null;
|
||||
return [...windows].sort((a, b) => {
|
||||
if (a.remainingPercent !== b.remainingPercent) {
|
||||
return a.remainingPercent - b.remainingPercent;
|
||||
}
|
||||
const aReset = toEpochMs(a.resetAt);
|
||||
const bReset = toEpochMs(b.resetAt);
|
||||
if (aReset === null && bReset === null) return 0;
|
||||
if (aReset === null) return 1;
|
||||
if (bReset === null) return -1;
|
||||
return aReset - bReset;
|
||||
})[0];
|
||||
}
|
||||
|
||||
function mapCoreWindow(window: ClaudeQuotaWindow | null): ClaudeCoreUsageSummary['fiveHour'] {
|
||||
if (!window) return null;
|
||||
return {
|
||||
rateLimitType: window.rateLimitType,
|
||||
label: window.label,
|
||||
remainingPercent: window.remainingPercent,
|
||||
resetAt: window.resetAt,
|
||||
status: window.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build explicit 5h + weekly usage summary from Claude policy windows.
|
||||
*/
|
||||
export function buildClaudeCoreUsageSummary(windows: ClaudeQuotaWindow[]): ClaudeCoreUsageSummary {
|
||||
if (!windows || windows.length === 0) {
|
||||
return { fiveHour: null, weekly: null };
|
||||
}
|
||||
|
||||
const fiveHourWindow = windows.find((window) => window.rateLimitType === 'five_hour') || null;
|
||||
const weeklyCandidates = windows.filter((window) =>
|
||||
['seven_day', 'seven_day_opus', 'seven_day_sonnet'].includes(window.rateLimitType)
|
||||
);
|
||||
const weeklyWindow = pickMostRestrictiveWeekly(weeklyCandidates);
|
||||
|
||||
// Fallback: infer shortest/longest reset windows from non-overage limits.
|
||||
if (!fiveHourWindow || !weeklyWindow) {
|
||||
const nonOverage = windows.filter((window) => window.rateLimitType !== 'overage');
|
||||
const withReset = nonOverage
|
||||
.map((window) => ({
|
||||
window,
|
||||
resetMs: toEpochMs(window.resetAt),
|
||||
}))
|
||||
.filter((entry) => entry.resetMs !== null)
|
||||
.sort((a, b) => (a.resetMs as number) - (b.resetMs as number));
|
||||
|
||||
const inferredFiveHour =
|
||||
fiveHourWindow ||
|
||||
(withReset.length > 0
|
||||
? withReset[0].window
|
||||
: nonOverage.length > 0
|
||||
? pickMostRestrictiveWeekly(nonOverage)
|
||||
: null);
|
||||
const inferredWeekly =
|
||||
weeklyWindow ||
|
||||
(withReset.length > 1
|
||||
? withReset[withReset.length - 1].window
|
||||
: nonOverage.find((window) => window !== inferredFiveHour) || null);
|
||||
|
||||
return {
|
||||
fiveHour: mapCoreWindow(inferredFiveHour),
|
||||
weekly: mapCoreWindow(inferredWeekly),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fiveHour: mapCoreWindow(fiveHourWindow),
|
||||
weekly: mapCoreWindow(weeklyWindow),
|
||||
};
|
||||
}
|
||||
@@ -4,13 +4,18 @@
|
||||
* Fetches policy limits from Claude API and normalizes 5h + weekly windows.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as fsp from 'node:fs/promises';
|
||||
import { getAuthDir } from './config-generator';
|
||||
import { getPausedDir, getProviderAccounts } from './account-manager';
|
||||
import { sanitizeEmail, isTokenExpired } from './auth-utils';
|
||||
import type { ClaudeQuotaResult, ClaudeQuotaWindow, ClaudeCoreUsageSummary } from './quota-types';
|
||||
import { clampPercent } from '../utils/percentage';
|
||||
import type { ClaudeQuotaResult } from './quota-types';
|
||||
import {
|
||||
buildClaudeQuotaWindows,
|
||||
buildClaudeCoreUsageSummary,
|
||||
} from './quota-fetcher-claude-normalizer';
|
||||
|
||||
export { buildClaudeQuotaWindows, buildClaudeCoreUsageSummary };
|
||||
|
||||
const CLAUDE_POLICY_LIMITS_URL = 'https://api.anthropic.com/api/claude_code/policy_limits';
|
||||
const CLAUDE_QUOTA_TIMEOUT_MS = 10000;
|
||||
@@ -26,288 +31,11 @@ function asString(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function asBoolean(value: unknown): boolean | undefined {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'string') {
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function asNumber(value: unknown): number | null {
|
||||
if (typeof value === 'number' && isFinite(value)) return value;
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
return isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeTimestamp(value: unknown): string | null {
|
||||
const asNum = asNumber(value);
|
||||
if (asNum !== null) {
|
||||
const millis = asNum > 1e12 ? asNum : asNum * 1000;
|
||||
const date = new Date(millis);
|
||||
return isNaN(date.getTime()) ? null : date.toISOString();
|
||||
}
|
||||
|
||||
const str = asString(value);
|
||||
if (!str) return null;
|
||||
|
||||
// Numeric strings can be either epoch seconds or epoch milliseconds.
|
||||
if (/^\d+$/.test(str)) {
|
||||
const numeric = Number(str);
|
||||
if (isFinite(numeric)) {
|
||||
const millis = numeric > 1e12 ? numeric : numeric * 1000;
|
||||
const date = new Date(millis);
|
||||
return isNaN(date.getTime()) ? null : date.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(str);
|
||||
return isNaN(date.getTime()) ? null : date.toISOString();
|
||||
}
|
||||
|
||||
function getClaudeWindowLabel(rateLimitType: string): string {
|
||||
switch (rateLimitType) {
|
||||
case 'five_hour':
|
||||
return 'Session limit';
|
||||
case 'seven_day':
|
||||
return 'Weekly limit';
|
||||
case 'seven_day_opus':
|
||||
return 'Opus limit';
|
||||
case 'seven_day_sonnet':
|
||||
return 'Sonnet limit';
|
||||
case 'overage':
|
||||
return 'Extra usage';
|
||||
default:
|
||||
return rateLimitType || 'Unknown limit';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUtilization(raw: Record<string, unknown>): {
|
||||
utilization: number | null;
|
||||
usedPercent: number;
|
||||
remainingPercent: number;
|
||||
} {
|
||||
const utilizationRaw = asNumber(raw['utilization']);
|
||||
const usedPercentRaw = asNumber(raw['usedPercent'] ?? raw['used_percent']);
|
||||
const remainingPercentRaw = asNumber(raw['remainingPercent'] ?? raw['remaining_percent']);
|
||||
|
||||
if (utilizationRaw !== null) {
|
||||
const ratio = utilizationRaw <= 1 ? utilizationRaw : utilizationRaw / 100;
|
||||
const usedPercent = clampPercent(ratio * 100);
|
||||
return {
|
||||
utilization: ratio,
|
||||
usedPercent,
|
||||
remainingPercent: clampPercent(100 - usedPercent),
|
||||
};
|
||||
}
|
||||
|
||||
if (usedPercentRaw !== null) {
|
||||
const usedPercent = clampPercent(usedPercentRaw);
|
||||
return {
|
||||
utilization: usedPercent / 100,
|
||||
usedPercent,
|
||||
remainingPercent: clampPercent(100 - usedPercent),
|
||||
};
|
||||
}
|
||||
|
||||
if (remainingPercentRaw !== null) {
|
||||
const remainingPercent = clampPercent(remainingPercentRaw);
|
||||
const usedPercent = clampPercent(100 - remainingPercent);
|
||||
return {
|
||||
utilization: usedPercent / 100,
|
||||
usedPercent,
|
||||
remainingPercent,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
utilization: null,
|
||||
usedPercent: 0,
|
||||
remainingPercent: 100,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRateLimitType(value: unknown, fallbackKey?: string): string {
|
||||
const direct = asString(value);
|
||||
if (direct) return direct;
|
||||
if (fallbackKey) return fallbackKey;
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function toObject(value: unknown): Record<string, unknown> | null {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function normalizeRestriction(
|
||||
raw: Record<string, unknown>,
|
||||
fallbackKey?: string
|
||||
): ClaudeQuotaWindow | null {
|
||||
const rateLimitType = normalizeRateLimitType(
|
||||
raw['rateLimitType'] ?? raw['rate_limit_type'] ?? raw['claim'] ?? raw['claimAbbrev'],
|
||||
fallbackKey
|
||||
);
|
||||
if (!rateLimitType || rateLimitType === 'unknown') return null;
|
||||
|
||||
const status = asString(raw['status']) || 'unknown';
|
||||
const resetAt =
|
||||
normalizeTimestamp(raw['resetsAt'] ?? raw['resets_at'] ?? raw['resetAt'] ?? raw['reset_at']) ||
|
||||
null;
|
||||
const overageResetsAt =
|
||||
normalizeTimestamp(
|
||||
raw['overageResetsAt'] ??
|
||||
raw['overage_resets_at'] ??
|
||||
raw['overageResetAt'] ??
|
||||
raw['overage_reset_at']
|
||||
) || null;
|
||||
|
||||
const { utilization, usedPercent, remainingPercent } = normalizeUtilization(raw);
|
||||
|
||||
return {
|
||||
rateLimitType,
|
||||
label: getClaudeWindowLabel(rateLimitType),
|
||||
status,
|
||||
utilization,
|
||||
usedPercent,
|
||||
remainingPercent,
|
||||
resetAt,
|
||||
surpassedThreshold: asBoolean(raw['surpassedThreshold'] ?? raw['surpassed_threshold']),
|
||||
severity: asString(raw['severity']) || undefined,
|
||||
overageStatus: asString(raw['overageStatus'] ?? raw['overage_status']) || undefined,
|
||||
overageResetsAt,
|
||||
overageDisabledReason:
|
||||
asString(raw['overageDisabledReason'] ?? raw['overage_disabled_reason']) || undefined,
|
||||
isUsingOverage: asBoolean(raw['isUsingOverage'] ?? raw['is_using_overage']),
|
||||
hasExtraUsageEnabled: asBoolean(raw['hasExtraUsageEnabled'] ?? raw['has_extra_usage_enabled']),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw policy limits response into normalized windows.
|
||||
* Supports both array and object-map `restrictions` shapes.
|
||||
*/
|
||||
export function buildClaudeQuotaWindows(payload: Record<string, unknown>): ClaudeQuotaWindow[] {
|
||||
const rawRestrictions = payload['restrictions'];
|
||||
const windows: ClaudeQuotaWindow[] = [];
|
||||
|
||||
if (Array.isArray(rawRestrictions)) {
|
||||
for (const item of rawRestrictions) {
|
||||
const raw = toObject(item);
|
||||
if (!raw) continue;
|
||||
const window = normalizeRestriction(raw);
|
||||
if (window) windows.push(window);
|
||||
}
|
||||
} else if (toObject(rawRestrictions)) {
|
||||
for (const [key, value] of Object.entries(rawRestrictions as Record<string, unknown>)) {
|
||||
const raw = toObject(value);
|
||||
if (!raw) continue;
|
||||
const window = normalizeRestriction(raw, key);
|
||||
if (window) windows.push(window);
|
||||
}
|
||||
} else if (toObject(payload)) {
|
||||
// Some responses may contain a single restriction object directly.
|
||||
const direct = normalizeRestriction(payload);
|
||||
if (direct) windows.push(direct);
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
const unique: ClaudeQuotaWindow[] = [];
|
||||
for (const window of windows) {
|
||||
const key = `${window.rateLimitType}:${window.resetAt ?? ''}:${window.status}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
unique.push(window);
|
||||
}
|
||||
|
||||
return unique.sort((a, b) => a.rateLimitType.localeCompare(b.rateLimitType));
|
||||
}
|
||||
|
||||
function toEpochMs(iso: string | null): number | null {
|
||||
if (!iso) return null;
|
||||
const value = new Date(iso).getTime();
|
||||
return isNaN(value) ? null : value;
|
||||
}
|
||||
|
||||
function pickMostRestrictiveWeekly(windows: ClaudeQuotaWindow[]): ClaudeQuotaWindow | null {
|
||||
if (windows.length === 0) return null;
|
||||
return [...windows].sort((a, b) => {
|
||||
if (a.remainingPercent !== b.remainingPercent) {
|
||||
return a.remainingPercent - b.remainingPercent;
|
||||
}
|
||||
const aReset = toEpochMs(a.resetAt);
|
||||
const bReset = toEpochMs(b.resetAt);
|
||||
if (aReset === null && bReset === null) return 0;
|
||||
if (aReset === null) return 1;
|
||||
if (bReset === null) return -1;
|
||||
return aReset - bReset;
|
||||
})[0];
|
||||
}
|
||||
|
||||
function mapCoreWindow(window: ClaudeQuotaWindow | null): ClaudeCoreUsageSummary['fiveHour'] {
|
||||
if (!window) return null;
|
||||
return {
|
||||
rateLimitType: window.rateLimitType,
|
||||
label: window.label,
|
||||
remainingPercent: window.remainingPercent,
|
||||
resetAt: window.resetAt,
|
||||
status: window.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build explicit 5h + weekly usage summary from Claude policy windows.
|
||||
*/
|
||||
export function buildClaudeCoreUsageSummary(windows: ClaudeQuotaWindow[]): ClaudeCoreUsageSummary {
|
||||
if (!windows || windows.length === 0) {
|
||||
return { fiveHour: null, weekly: null };
|
||||
}
|
||||
|
||||
const fiveHourWindow = windows.find((window) => window.rateLimitType === 'five_hour') || null;
|
||||
const weeklyCandidates = windows.filter((window) =>
|
||||
['seven_day', 'seven_day_opus', 'seven_day_sonnet'].includes(window.rateLimitType)
|
||||
);
|
||||
const weeklyWindow = pickMostRestrictiveWeekly(weeklyCandidates);
|
||||
|
||||
// Fallback: infer shortest/longest reset windows from non-overage limits.
|
||||
if (!fiveHourWindow || !weeklyWindow) {
|
||||
const nonOverage = windows.filter((window) => window.rateLimitType !== 'overage');
|
||||
const withReset = nonOverage
|
||||
.map((window) => ({
|
||||
window,
|
||||
resetMs: toEpochMs(window.resetAt),
|
||||
}))
|
||||
.filter((entry) => entry.resetMs !== null)
|
||||
.sort((a, b) => (a.resetMs as number) - (b.resetMs as number));
|
||||
|
||||
const inferredFiveHour =
|
||||
fiveHourWindow ||
|
||||
(withReset.length > 0
|
||||
? withReset[0].window
|
||||
: nonOverage.length > 0
|
||||
? pickMostRestrictiveWeekly(nonOverage)
|
||||
: null);
|
||||
const inferredWeekly =
|
||||
weeklyWindow ||
|
||||
(withReset.length > 1
|
||||
? withReset[withReset.length - 1].window
|
||||
: nonOverage.find((window) => window !== inferredFiveHour) || null);
|
||||
|
||||
return {
|
||||
fiveHour: mapCoreWindow(inferredFiveHour),
|
||||
weekly: mapCoreWindow(inferredWeekly),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fiveHour: mapCoreWindow(fiveHourWindow),
|
||||
weekly: mapCoreWindow(weeklyWindow),
|
||||
};
|
||||
}
|
||||
|
||||
function extractAccessToken(data: Record<string, unknown>): string | null {
|
||||
const direct = asString(data['access_token']);
|
||||
if (direct) return direct;
|
||||
@@ -333,34 +61,51 @@ function extractExpiry(data: Record<string, unknown>): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function readClaudeAuthData(accountId: string): ClaudeAuthData | null {
|
||||
async function readJsonFile(filePath: string): Promise<Record<string, unknown> | null> {
|
||||
try {
|
||||
const raw = await fsp.readFile(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
return toObject(parsed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function readAuthCandidate(filePath: string): Promise<ClaudeAuthData | null> {
|
||||
const data = await readJsonFile(filePath);
|
||||
if (!data) return null;
|
||||
|
||||
const accessToken = extractAccessToken(data);
|
||||
if (!accessToken) return null;
|
||||
|
||||
const expiry = extractExpiry(data);
|
||||
return {
|
||||
accessToken,
|
||||
isExpired: isTokenExpired(expiry ?? undefined),
|
||||
};
|
||||
}
|
||||
|
||||
async function readClaudeAuthData(accountId: string): Promise<ClaudeAuthData | null> {
|
||||
const authDirs = [getAuthDir(), getPausedDir()];
|
||||
const sanitizedId = sanitizeEmail(accountId);
|
||||
const expectedFiles = [`claude-${sanitizedId}.json`, `anthropic-${sanitizedId}.json`];
|
||||
|
||||
for (const authDir of authDirs) {
|
||||
if (!fs.existsSync(authDir)) continue;
|
||||
|
||||
for (const expectedFile of expectedFiles) {
|
||||
const filePath = path.join(authDir, expectedFile);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, unknown>;
|
||||
const accessToken = extractAccessToken(data);
|
||||
if (!accessToken) continue;
|
||||
|
||||
const expiry = extractExpiry(data);
|
||||
return {
|
||||
accessToken,
|
||||
isExpired: isTokenExpired(expiry ?? undefined),
|
||||
};
|
||||
} catch {
|
||||
continue;
|
||||
const authData = await readAuthCandidate(filePath);
|
||||
if (authData) {
|
||||
return authData;
|
||||
}
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(authDir);
|
||||
let files: string[];
|
||||
try {
|
||||
files = await fsp.readdir(authDir);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
if (
|
||||
!file.endsWith('.json') ||
|
||||
@@ -370,27 +115,25 @@ function readClaudeAuthData(accountId: string): ClaudeAuthData | null {
|
||||
}
|
||||
|
||||
const filePath = path.join(authDir, file);
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, unknown>;
|
||||
const accessToken = extractAccessToken(data);
|
||||
if (!accessToken) continue;
|
||||
const data = await readJsonFile(filePath);
|
||||
if (!data) continue;
|
||||
|
||||
const fileEmail = asString(data['email']);
|
||||
const typeValue = asString(data['type']);
|
||||
const isClaudeType =
|
||||
typeValue === null || typeValue === 'claude' || typeValue === 'anthropic';
|
||||
const matchesEmail = fileEmail === accountId;
|
||||
const matchesFile = file.includes(sanitizedId);
|
||||
const accessToken = extractAccessToken(data);
|
||||
if (!accessToken) continue;
|
||||
|
||||
if ((matchesEmail || matchesFile) && isClaudeType) {
|
||||
const expiry = extractExpiry(data);
|
||||
return {
|
||||
accessToken,
|
||||
isExpired: isTokenExpired(expiry ?? undefined),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
const fileEmail = asString(data['email']);
|
||||
const typeValue = asString(data['type']);
|
||||
const isClaudeType =
|
||||
typeValue === null || typeValue === 'claude' || typeValue === 'anthropic';
|
||||
const matchesEmail = fileEmail === accountId;
|
||||
const matchesFile = file.includes(sanitizedId);
|
||||
|
||||
if ((matchesEmail || matchesFile) && isClaudeType) {
|
||||
const expiry = extractExpiry(data);
|
||||
return {
|
||||
accessToken,
|
||||
isExpired: isTokenExpired(expiry ?? undefined),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +164,7 @@ export async function fetchClaudeQuota(
|
||||
accountId: string,
|
||||
verbose = false
|
||||
): Promise<ClaudeQuotaResult> {
|
||||
const authData = readClaudeAuthData(accountId);
|
||||
const authData = await readClaudeAuthData(accountId);
|
||||
if (!authData) {
|
||||
return buildEmptyResult('Auth file not found for Claude account', accountId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user