feat: add Claude reset grants functionality

- Implemented ClaudeResetGrants component to manage reset grants for Claude usage.
- Integrated reset grant operations with session management and eligibility checks.
- Added API service for reading and claiming reset grants from the Anthropic API.
- Enhanced UI with localized messages for reset grant actions.
- Created tests for grant parsing, claiming, and session handling to ensure reliability.
This commit is contained in:
Supra4E8C
2026-10-01 21:58:20 +08:00
parent a7ec312fbb
commit d554bb0983
10 changed files with 1015 additions and 0 deletions
@@ -22,6 +22,7 @@ import {
import { bindQuotaClasses } from '../types';
import { QUOTA_ADAPTERS, type QuotaCardState } from '../providers';
import { isQuotaRefreshDisabled, type QuotaFileEntry } from '../logic';
import { ClaudeResetGrants } from '../providers/claude/ClaudeResetGrants';
import bodyStyles from './QuotaBody.module.scss';
import styles from './QuotaCard.module.scss';
@@ -138,6 +139,13 @@ export function QuotaCard(props: QuotaCardProps) {
{status !== 'idle' && (
<footer className={styles.actionRow}>
{entry.type === 'claude' && (
<ClaudeResetGrants
file={file}
disabled={!canRefresh || loading || resetting}
onRefresh={onRefresh}
/>
)}
{showReset && (
<button
type="button"
@@ -0,0 +1,183 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal';
import { useNow } from '@/hooks/useNow';
import { useAuthStore } from '@/stores/useAuthStore';
import { apiClient } from '@/services/api/client';
import {
anthropicResetGrantBlocker,
readClaudeResetGrants,
type AnthropicResetGrantStatus,
} from '@/services/api/claudeResetGrants';
import type { AuthFileItem } from '@/types';
import { normalizeAuthIndex } from '@/utils/quota';
import { resetGrantOperations, RETRY_WINDOW_MS } from './resetGrantOperations';
export function ClaudeResetGrants({
file,
disabled: externallyDisabled,
onRefresh,
}: {
file: AuthFileItem;
disabled: boolean;
onRefresh: () => void;
}) {
const { t } = useTranslation();
const connectionStatus = useAuthStore((state) => state.connectionStatus);
const [session] = useState(() => apiClient.getConnectionRevision());
const sessionActive =
connectionStatus === 'connected' && session === apiClient.getConnectionRevision();
const disabled = externallyDisabled || !sessionActive;
const now = useNow();
const authIndex = normalizeAuthIndex(file.auth_index ?? file.authIndex);
const [open, setOpen] = useState(false);
const [status, setStatus] = useState<AnthropicResetGrantStatus | null>(null);
const [selected, setSelected] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState('');
const lock = useRef(false);
const generation = useRef(0);
useEffect(
() => () => {
generation.current += 1;
},
[authIndex, file.name]
);
const key = JSON.stringify([file.name, authIndex]);
const operation = resetGrantOperations.inspect(key);
const pending = operation && !operation.code ? operation : undefined;
const expired = pending && now - pending.createdAt >= RETRY_WINDOW_MS;
const load = async () => {
if (!authIndex || disabled || lock.current) return;
lock.current = true;
setBusy(true);
setOpen(true);
setStatus(null);
setSelected(null);
setMessage('');
const revision = apiClient.getConnectionRevision();
const version = generation.current;
const current = () =>
revision === apiClient.getConnectionRevision() && version === generation.current;
try {
const result = await readClaudeResetGrants(authIndex);
if (current()) setStatus(result);
} catch {
if (current()) setMessage('read_error');
} finally {
lock.current = false;
if (current()) setBusy(false);
}
};
const consume = async () => {
if (!authIndex || !selected || disabled || lock.current) return;
lock.current = true;
setBusy(true);
setMessage('');
const revision = apiClient.getConnectionRevision();
const version = generation.current;
const current = () =>
revision === apiClient.getConnectionRevision() && version === generation.current;
try {
const answer = await resetGrantOperations.run(key, authIndex, selected);
if (!current()) return;
setMessage(answer.unresolved ? 'unknown' : answer.code);
setSelected(null);
setStatus(null);
onRefresh();
} catch {
if (current()) {
const unresolved = resetGrantOperations.inspect(key);
setMessage(unresolved && !unresolved.code ? 'unknown' : 'blocked');
setSelected(null);
}
} finally {
lock.current = false;
if (current()) setBusy(false);
}
};
return (
<>
<Button
size="sm"
variant="secondary"
disabled={disabled || !authIndex || busy}
onClick={() => void load()}
>
{t('claude_reset.title')}
</Button>
<Modal
open={open && sessionActive}
title={t('claude_reset.title')}
onClose={() => setOpen(false)}
closeDisabled={busy}
footer={
selected ? (
<Button disabled={disabled || busy} loading={busy} onClick={() => void consume()}>
{t('claude_reset.confirm')}
</Button>
) : undefined
}
>
<p>{t('claude_reset.scope')}</p>
<div role="status" aria-live="polite">
{busy && <p>{t('claude_reset.loading')}</p>}
{message && <p>{t(`claude_reset.${message}`)}</p>}
</div>
{pending ? (
<>
<p>{t(expired ? 'claude_reset.expired' : 'claude_reset.unknown')}</p>
<Button
variant="secondary"
disabled={Boolean(expired) || busy || disabled}
onClick={() => setSelected(pending.grantId)}
>
{t('claude_reset.retry')}
</Button>
</>
) : (
status && (
<>
{!status.eligible && <p>{t('claude_reset.ineligible')}</p>}
{status.grants.length === 0 && <p>{t('claude_reset.empty')}</p>}
{status.grants.map((grant) => (
<div key={grant.id}>
<p>
{grant.label || grant.id} —{' '}
{t('claude_reset.count', { left: grant.resetsLeft, total: grant.resetsTotal })}
</p>
{grant.endsAt && (
<p>
{t('claude_reset.ends', { date: new Date(grant.endsAt).toLocaleString() })}
</p>
)}
<Button
variant="secondary"
size="sm"
disabled={
busy || disabled || Boolean(anthropicResetGrantBlocker(status, grant.id))
}
onClick={() => setSelected(grant.id)}
>
{t('claude_reset.use')}
</Button>
</div>
))}
</>
)
)}
{selected && (
<p>
{t('claude_reset.confirm_text', {
grant: status?.grants.find((grant) => grant.id === selected)?.label || selected,
})}
</p>
)}
</Modal>
</>
);
}
@@ -0,0 +1,101 @@
import { apiClient } from '@/services/api/client';
import {
anthropicResetGrantBlocker,
claimClaudeResetGrant,
readClaudeOrganization,
readClaudeResetGrants,
type AnthropicResetSettledCode,
} from '@/services/api/claudeResetGrants';
export const RETRY_WINDOW_MS = 10 * 60 * 1000;
type Operation = {
grantId: string;
organization: string;
requestId: string;
createdAt: number;
code?: AnthropicResetSettledCode;
};
const defaultDependencies = {
revision: () => apiClient.getConnectionRevision(),
readStatus: readClaudeResetGrants,
readOrganization: readClaudeOrganization,
claim: claimClaudeResetGrant,
now: () => Date.now(),
requestId: () => crypto.randomUUID() as string,
};
/** Tab-memory journal: survives dialog/card unmounts, never crosses connections.
* No automatic retry. Expired ambiguous operations stay blocked until session end.
* This is not a cross-tab or durable backend spending ledger.
*/
export function createResetGrantOperations(deps = defaultDependencies) {
let revision = deps.revision();
const operations = new Map<string, Operation>();
const busy = new Set<string>();
const syncSession = () => {
if (revision !== deps.revision()) {
revision = deps.revision();
operations.clear();
busy.clear();
}
return revision;
};
return {
inspect(key: string) {
syncSession();
return operations.get(key);
},
async run(key: string, authIndex: string, grantId: string) {
const session = syncSession();
const assertSession = () => {
if (deps.revision() !== session) throw new Error('session');
};
if (busy.has(key)) throw new Error('busy');
busy.add(key);
try {
let operation = operations.get(key);
if (operation?.code) operation = undefined;
if (
operation &&
(operation.grantId !== grantId || deps.now() - operation.createdAt >= RETRY_WINDOW_MS)
)
throw new Error('expired');
const wasRetry = Boolean(operation);
const organization = await deps.readOrganization(authIndex);
assertSession();
if (operation && operation.organization !== organization) throw new Error('identity');
if (!operation) {
const status = await deps.readStatus(authIndex);
assertSession();
const grant = status.grants.find((item) => item.id === grantId);
const now = deps.now();
if (
anthropicResetGrantBlocker(status, grantId) ||
!grant ||
(grant.startsAt && Date.parse(grant.startsAt) > now) ||
(grant.endsAt && Date.parse(grant.endsAt) <= now) ||
(status.cooldownUntil && Date.parse(status.cooldownUntil) > now)
) {
throw new Error('blocked');
}
operation = { grantId, organization, requestId: deps.requestId(), createdAt: now };
operations.set(key, operation);
}
// Re-check immediately before dispatch; never send against a replacement connection.
assertSession();
if (wasRetry && deps.now() - operation.createdAt >= RETRY_WINDOW_MS) {
throw new Error('expired');
}
const code = await deps.claim(authIndex, organization, grantId, operation.requestId);
assertSession();
// A refusal on a retry cannot prove that the earlier ambiguous POST did not spend.
if (!wasRetry || code === 'reset' || code === 'already_used') operation.code = code;
return { code, unresolved: !operation.code };
} finally {
if (deps.revision() === session) busy.delete(key);
}
},
};
}
export const resetGrantOperations = createResetGrantOperations();
+24
View File
@@ -2199,5 +2199,29 @@
"modelCatalog": {
"notLoaded": "Not loaded"
}
},
"claude_reset": {
"title": "Banked resets",
"scope": "Uses a limited reset grant to clear eligible Claude usage windows. Only confirm when you want to spend a reset. Recovery is limited to this tab/session; do not reload or use another tab while an outcome is unknown.",
"loading": "Loading…",
"read_error": "Reset grants could not be read. Nothing was submitted.",
"blocked": "Reset not submitted: eligibility, account identity, or session could not be verified. Refresh and check again.",
"unknown": "The outcome is unknown. A reset may have been spent. Retry within 10 minutes using the same request ID; do not start another claim elsewhere.",
"expired": "The retry window has expired. Further claims are blocked in this session. Check your Claude account before taking any further action.",
"retry": "Retry the same claim",
"empty": "No banked reset grants.",
"count": "{{left}} / {{total}} resets remaining",
"ends": "Expires: {{date}}",
"use": "Use reset",
"confirm": "Confirm reset",
"confirm_text": "Spend one reset from “{{grant}}”? An unknown claim will be retried with its original request ID.",
"reset": "Usage limits reset.",
"already_used": "This grant was already used.",
"not_limited": "No reset: the account is not currently limited.",
"cooldown": "No reset: cooldown is active.",
"ineligible": "This account is not eligible.",
"unavailable": "Resets are currently unavailable.",
"rate_limited": "Request rate limited.",
"auth_error": "Authorization was refused."
}
}
+24
View File
@@ -2193,5 +2193,29 @@
"modelCatalog": {
"notLoaded": "Не загружено"
}
},
"claude_reset": {
"title": "Запас сбросов",
"scope": "Использует ограниченное право на сброс лимитов Claude. Подтверждайте только если хотите потратить сброс. Восстановление доступно только в этой вкладке и сессии; при неизвестном результате не перезагружайте страницу и не используйте другую вкладку.",
"loading": "Загрузка…",
"read_error": "Не удалось прочитать права на сброс. Запрос сброса не отправлен.",
"blocked": "Сброс не отправлен: не удалось проверить условия, учётную запись или сессию. Обновите данные.",
"unknown": "Результат неизвестен. Сброс мог быть потрачен. Повторите в течение 10 минут с тем же ID запроса; не создавайте новый запрос в другом месте.",
"expired": "Окно повтора истекло. Дальнейшие сбросы в этой сессии заблокированы. Сначала проверьте учётную запись Claude.",
"retry": "Повторить тот же запрос",
"empty": "Нет доступных запасных сбросов.",
"count": "Осталось сбросов: {{left}} / {{total}}",
"ends": "Истекает: {{date}}",
"use": "Использовать сброс",
"confirm": "Подтвердить сброс",
"confirm_text": "Потратить один сброс «{{grant}}»? Запрос с неизвестным результатом будет повторён с исходным ID.",
"reset": "Лимиты использования сброшены.",
"already_used": "Это право уже использовано.",
"not_limited": "Сброс не выполнен: лимит ещё не достигнут.",
"cooldown": "Сброс не выполнен: действует период ожидания.",
"ineligible": "Эта учётная запись не соответствует условиям.",
"unavailable": "Сбросы временно недоступны.",
"rate_limited": "Слишком много запросов.",
"auth_error": "Авторизация отклонена."
}
}
+24
View File
@@ -2197,5 +2197,29 @@
"modelCatalog": {
"notLoaded": "未加载"
}
},
"claude_reset": {
"title": "储备重置次数",
"scope": "使用有限的重置权益清除符合条件的 Claude 用量窗口。仅在确定要消耗一次重置时确认。恢复仅限当前标签页和会话;结果未知时,请勿刷新页面或使用其他标签页。",
"loading": "加载中…",
"read_error": "无法读取重置权益,未提交重置。",
"blocked": "未提交重置:无法验证资格、账户身份或会话。请刷新后检查。",
"unknown": "结果未知,可能已消耗一次重置。请在 10 分钟内使用相同请求 ID 重试,不要在其他位置发起新的重置。",
"expired": "重试窗口已过期,当前会话已阻止进一步重置。请先检查 Claude 账户状态。",
"retry": "重试同一请求",
"empty": "没有储备重置权益。",
"count": "剩余 {{left}} / {{total}} 次重置",
"ends": "到期:{{date}}",
"use": "使用重置",
"confirm": "确认重置",
"confirm_text": "消耗“{{grant}}”的一次重置?结果未知的请求将使用原请求 ID 重试。",
"reset": "已重置用量限制。",
"already_used": "该权益已使用。",
"not_limited": "未重置:账户当前未达到限制。",
"cooldown": "未重置:仍在冷却期。",
"ineligible": "此账户不符合资格。",
"unavailable": "重置暂不可用。",
"rate_limited": "请求过于频繁。",
"auth_error": "授权被拒绝。"
}
}
+24
View File
@@ -2223,5 +2223,29 @@
"modelCatalog": {
"notLoaded": "未載入"
}
},
"claude_reset": {
"title": "儲備重置次數",
"scope": "使用有限的重置權益清除符合條件的 Claude 用量視窗。僅在確定要消耗一次重置時確認。復原僅限目前分頁和工作階段;結果未知時,請勿重新載入頁面或使用其他分頁。",
"loading": "載入中…",
"read_error": "無法讀取重置權益,未提交重置。",
"blocked": "未提交重置:無法驗證資格、帳戶身分或工作階段。請重新整理後檢查。",
"unknown": "結果未知,可能已消耗一次重置。請在 10 分鐘內使用相同請求 ID 重試,不要在其他位置發起新的重置。",
"expired": "重試視窗已過期,目前工作階段已阻止進一步重置。請先檢查 Claude 帳戶狀態。",
"retry": "重試同一請求",
"empty": "沒有儲備重置權益。",
"count": "剩餘 {{left}} / {{total}} 次重置",
"ends": "到期:{{date}}",
"use": "使用重置",
"confirm": "確認重置",
"confirm_text": "消耗「{{grant}}」的一次重置?結果未知的請求將使用原請求 ID 重試。",
"reset": "已重置用量限制。",
"already_used": "該權益已使用。",
"not_limited": "未重置:帳戶目前未達到限制。",
"cooldown": "未重置:仍在冷卻期。",
"ineligible": "此帳戶不符合資格。",
"unavailable": "重置暫不可用。",
"rate_limited": "請求過於頻繁。",
"auth_error": "授權遭拒。"
}
}
+312
View File
@@ -0,0 +1,312 @@
// Upstream cedar_ember contract, following opencodex anthropic-reset-grants.
import { apiCallApi } from './apiCall';
import { CLAUDE_REQUEST_HEADERS } from '@/utils/quota/constants';
export const ANTHROPIC_API_ORIGIN = 'https://api.anthropic.com';
export const ANTHROPIC_RESET_GRANT_PROGRAM = 'cedar_ember';
export const ANTHROPIC_RESET_GRANT_STATUS_PATH = '/api/oauth/usage?cedar_ember=1&skip_spend=1';
export const ANTHROPIC_PROFILE_PATH = '/api/oauth/profile';
/** Same bound the Claude Code client uses for the claim. */
export const ANTHROPIC_RESET_GRANT_REDEEM_TIMEOUT_MS = 25_000;
export const ANTHROPIC_RESET_GRANT_ID_RE = /^[a-z0-9_-]{1,40}$/;
export const ANTHROPIC_RESET_REQUEST_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
export const ORGANIZATION_UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/** Usage windows a grant can clear. Others upstream may add are dropped. */
export const ANTHROPIC_RESET_WINDOWS = [
'five_hour',
'seven_day',
'seven_day_overage_included',
] as const;
export type AnthropicResetWindow = (typeof ANTHROPIC_RESET_WINDOWS)[number];
/** Terminal answers the claim endpoint can give. */
export const ANTHROPIC_RESET_RESULTS = [
'reset',
'already_used',
'not_limited',
'cooldown',
'ineligible',
'unavailable',
] as const;
export type AnthropicResetUpstreamResult = (typeof ANTHROPIC_RESET_RESULTS)[number];
/** Upstream results plus the two HTTP refusals that prove nothing was spent. */
export type AnthropicResetSettledCode =
AnthropicResetUpstreamResult | 'rate_limited' | 'auth_error';
export interface AnthropicResetGrant {
id: string;
label: string;
resetsTotal: number;
resetsLeft: number;
startsAt: string | null;
endsAt: string | null;
clears: AnthropicResetWindow[];
paused: boolean;
usableNow: boolean;
useRequiresLimit: boolean;
percentUsed: Partial<Record<AnthropicResetWindow, number>>;
}
export interface AnthropicResetGrantStatus {
eligible: boolean;
ineligibleReason: string | null;
atLimit: boolean;
grants: AnthropicResetGrant[];
nextGrantId: string | null;
weeklyResetsAt: string | null;
cooldownUntil: string | null;
}
export type AnthropicResetGrantErrorCode = 'auth' | 'upstream' | 'malformed';
/** A read failure. `message` is fixed text; upstream detail is never attached. */
export class AnthropicResetGrantError extends Error {
constructor(readonly code: AnthropicResetGrantErrorCode) {
super(`Anthropic reset-grant read failed (${code})`);
this.name = 'AnthropicResetGrantError';
}
}
/** The claim was sent (or may have been) and no terminal answer came back. */
export class AnthropicResetGrantUnknownOutcome extends Error {
constructor() {
super('Anthropic reset-grant claim outcome is unknown');
this.name = 'AnthropicResetGrantUnknownOutcome';
}
}
const KNOWN_INELIGIBLE_REASONS = new Set([
'config_off',
'tier',
'seat',
'mobile',
'surface',
'cli_version',
'no_grant',
'tenure',
'other_experiment',
'unavailable',
'unknown',
]);
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isCount(value: unknown): value is number {
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
}
/** Optional ISO timestamp: absent/null → null, anything unparsable rejects the block. */
function optionalTimestamp(value: unknown): string | null | undefined {
if (value === undefined || value === null) return null;
if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) return undefined;
return value;
}
function optionalBoolean(value: unknown, fallback: boolean): boolean | undefined {
if (value === undefined || value === null) return fallback;
return typeof value === 'boolean' ? value : undefined;
}
/** Upstream display text, stripped of control characters and bounded. */
function safeLabel(value: unknown): string {
if (typeof value !== 'string') return '';
return (
value
// Strip untrusted display control characters.
// eslint-disable-next-line no-control-regex
.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 120)
);
}
function parseWindows(value: unknown): AnthropicResetWindow[] | undefined {
if (value === undefined || value === null) return [];
if (!Array.isArray(value)) return undefined;
return ANTHROPIC_RESET_WINDOWS.filter((window) => value.includes(window));
}
function parsePercentUsed(value: unknown): Partial<Record<AnthropicResetWindow, number>> {
const out: Partial<Record<AnthropicResetWindow, number>> = {};
if (!isRecord(value)) return out;
for (const window of ANTHROPIC_RESET_WINDOWS) {
const percent = value[window];
if (typeof percent === 'number' && Number.isInteger(percent) && percent >= 0 && percent <= 100)
out[window] = percent;
}
return out;
}
function parseGrant(value: unknown): AnthropicResetGrant | null {
if (!isRecord(value)) return null;
const { id, resets_total: total, resets_left: left } = value;
if (typeof id !== 'string' || !ANTHROPIC_RESET_GRANT_ID_RE.test(id)) return null;
if (!isCount(total) || !isCount(left) || left > total) return null;
const startsAt = optionalTimestamp(value.starts_at);
const endsAt = optionalTimestamp(value.ends_at);
const clears = parseWindows(value.clears);
const paused = optionalBoolean(value.paused, false);
// Missing usability flags default to the refusing side: a grant that does not
// say it is usable is not offered for spending.
const usableNow = optionalBoolean(value.usable_now, false);
const useRequiresLimit = optionalBoolean(value.use_requires_limit, true);
if (
startsAt === undefined ||
endsAt === undefined ||
clears === undefined ||
paused === undefined ||
usableNow === undefined ||
useRequiresLimit === undefined
)
return null;
return {
id,
label: safeLabel(value.label),
resetsTotal: total,
resetsLeft: left,
startsAt,
endsAt,
clears,
paused,
usableNow,
useRequiresLimit,
percentUsed: parsePercentUsed(value.percent_used),
};
}
/**
* Parses the `cedar_ember` block. Returns null for a missing or malformed block;
* one malformed grant, a duplicate id, or a bad timestamp rejects the whole block.
*/
export function parseAnthropicResetGrantStatus(block: unknown): AnthropicResetGrantStatus | null {
if (!isRecord(block) || typeof block.eligible !== 'boolean') return null;
const rawGrants = block.grants ?? [];
if (!Array.isArray(rawGrants)) return null;
const grants: AnthropicResetGrant[] = [];
const seen = new Set<string>();
for (const raw of rawGrants) {
const grant = parseGrant(raw);
if (!grant || seen.has(grant.id)) return null;
seen.add(grant.id);
grants.push(grant);
}
const reason = block.ineligible_reason;
if (reason !== undefined && reason !== null && typeof reason !== 'string') return null;
const atLimit = optionalBoolean(block.at_limit, false);
const weeklyResetsAt = optionalTimestamp(block.weekly_resets_at);
const cooldownUntil = optionalTimestamp(block.cooldown_until);
if (atLimit === undefined || weeklyResetsAt === undefined || cooldownUntil === undefined)
return null;
const next = block.next_grant_id;
return {
eligible: block.eligible,
ineligibleReason:
typeof reason === 'string'
? KNOWN_INELIGIBLE_REASONS.has(reason)
? reason
: 'unknown'
: null,
atLimit,
grants,
nextGrantId: typeof next === 'string' && seen.has(next) ? next : null,
weeklyResetsAt,
cooldownUntil,
};
}
/** Why a grant cannot be spent right now, or null when it can. */
export function anthropicResetGrantBlocker(
status: AnthropicResetGrantStatus,
grantId: string
): 'ineligible' | 'unknown_grant' | 'paused' | 'not_usable' | 'exhausted' | 'not_limited' | null {
if (!status.eligible) return 'ineligible';
const grant = status.grants.find((candidate) => candidate.id === grantId);
if (!grant) return 'unknown_grant';
if (grant.paused) return 'paused';
if (!grant.usableNow) return 'not_usable';
if (grant.resetsLeft <= 0) return 'exhausted';
if (grant.useRequiresLimit && !status.atLimit) return 'not_limited';
return null;
}
async function readAccount(authIndex: string, path: string): Promise<Record<string, unknown>> {
const response = await apiCallApi.request(
{
authIndex,
method: 'GET',
url: ANTHROPIC_API_ORIGIN + path,
header: { ...CLAUDE_REQUEST_HEADERS },
},
{ timeout: 12000 }
);
if (response.statusCode < 200 || response.statusCode >= 300 || !isRecord(response.body)) {
throw new AnthropicResetGrantError('upstream');
}
return response.body;
}
export async function readClaudeResetGrants(authIndex: string) {
const body = await readAccount(authIndex, ANTHROPIC_RESET_GRANT_STATUS_PATH);
const status = parseAnthropicResetGrantStatus(body.cedar_ember);
if (!status) throw new AnthropicResetGrantError('malformed');
return status;
}
export async function readClaudeOrganization(authIndex: string) {
const body = await readAccount(authIndex, ANTHROPIC_PROFILE_PATH);
const uuid = isRecord(body.organization) ? body.organization.uuid : undefined;
if (typeof uuid !== 'string' || !ORGANIZATION_UUID_RE.test(uuid)) {
throw new AnthropicResetGrantError('malformed');
}
return uuid.toLowerCase();
}
export async function claimClaudeResetGrant(
authIndex: string,
organization: string,
grantId: string,
requestId: string
): Promise<AnthropicResetSettledCode> {
if (
!ORGANIZATION_UUID_RE.test(organization) ||
!ANTHROPIC_RESET_GRANT_ID_RE.test(grantId) ||
!ANTHROPIC_RESET_REQUEST_ID_RE.test(requestId)
)
throw new AnthropicResetGrantError('malformed');
try {
const response = await apiCallApi.request(
{
authIndex,
method: 'POST',
url: `${ANTHROPIC_API_ORIGIN}/api/organizations/${organization}/reset_rate_limits`,
header: { ...CLAUDE_REQUEST_HEADERS },
data: JSON.stringify({
program: ANTHROPIC_RESET_GRANT_PROGRAM,
grant_id: grantId,
request_id: requestId,
}),
},
{ timeout: ANTHROPIC_RESET_GRANT_REDEEM_TIMEOUT_MS }
);
if (response.statusCode === 429) return 'rate_limited';
if (response.statusCode === 401 || response.statusCode === 403) return 'auth_error';
const result = isRecord(response.body) ? response.body.result : undefined;
if (
response.statusCode >= 200 &&
response.statusCode < 300 &&
typeof result === 'string' &&
(ANTHROPIC_RESET_RESULTS as readonly string[]).includes(result)
) {
return result as AnthropicResetUpstreamResult;
}
} catch {
/* The proxy may have sent the claim even if the management request failed. */
}
throw new AnthropicResetGrantUnknownOutcome();
}
+1
View File
@@ -110,6 +110,7 @@ export const CLAUDE_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
export const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
export const CLAUDE_REQUEST_HEADERS = {
'User-Agent': 'claude-cli/2.1.280 (external, cli)',
Authorization: 'Bearer $TOKEN$',
'Content-Type': 'application/json',
'anthropic-beta': 'oauth-2025-04-20',
+314
View File
@@ -0,0 +1,314 @@
import { afterEach, describe, expect, test } from 'bun:test';
import {
anthropicResetGrantBlocker,
claimClaudeResetGrant,
parseAnthropicResetGrantStatus,
readClaudeOrganization,
readClaudeResetGrants,
AnthropicResetGrantUnknownOutcome,
ANTHROPIC_RESET_RESULTS,
type AnthropicResetSettledCode,
} from '../src/services/api/claudeResetGrants';
import { apiCallApi, type ApiCallRequest } from '../src/services/api/apiCall';
import {
createResetGrantOperations,
RETRY_WINDOW_MS,
} from '../src/features/quota/providers/claude/resetGrantOperations';
const organization = '11111111-2222-3333-4444-555555555555';
const grant = { id: 'test-grant', resets_total: 2, resets_left: 2, usable_now: true };
const block = { eligible: true, at_limit: true, grants: [grant] };
const status = () => parseAnthropicResetGrantStatus(block)!;
const originalRequest = apiCallApi.request;
afterEach(() => {
apiCallApi.request = originalRequest;
});
describe('Claude reset grant fail-closed parsing', () => {
test('valid block and refusing defaults', () => {
expect(status().grants[0].useRequiresLimit).toBe(true);
expect(anthropicResetGrantBlocker(status(), grant.id)).toBeNull();
expect(anthropicResetGrantBlocker({ ...status(), atLimit: false }, grant.id)).toBe(
'not_limited'
);
expect(anthropicResetGrantBlocker({ ...status(), eligible: false }, grant.id)).toBe(
'ineligible'
);
const missing = parseAnthropicResetGrantStatus({
...block,
grants: [{ ...grant, usable_now: undefined }],
})!;
expect(anthropicResetGrantBlocker(missing, grant.id)).toBe('not_usable');
});
test('rejects malformed blocks and grants', () => {
for (const value of [
undefined,
{},
{ ...block, eligible: 1 },
{ ...block, grants: {} },
{ ...block, grants: [grant, grant] },
{ ...block, cooldown_until: 'bad' },
]) {
expect(parseAnthropicResetGrantStatus(value)).toBeNull();
}
for (const fields of [
{ id: '../bad' },
{ resets_left: -1 },
{ resets_left: 3 },
{ resets_total: 1.5 },
{ paused: 'false' },
{ starts_at: 'bad' },
{ clears: {} },
]) {
expect(
parseAnthropicResetGrantStatus({ ...block, grants: [{ ...grant, ...fields }] })
).toBeNull();
}
});
});
test('exact proxied GET/profile/claim contract, no provider token in frontend', async () => {
const calls: ApiCallRequest[] = [];
apiCallApi.request = async (request) => {
calls.push(request);
const body =
request.method === 'POST'
? { result: 'reset' }
: request.url.endsWith('profile')
? { organization: { uuid: organization } }
: { cedar_ember: block };
return { statusCode: 200, body, bodyText: '', header: {} };
};
await readClaudeResetGrants('index');
expect(await readClaudeOrganization('index')).toBe(organization);
expect(await claimClaudeResetGrant('index', organization, grant.id, 'request-1')).toBe('reset');
expect(calls[0].url).toBe('https://api.anthropic.com/api/oauth/usage?cedar_ember=1&skip_spend=1');
expect(calls[1].url).toBe('https://api.anthropic.com/api/oauth/profile');
expect(calls[2].url).toBe(
`https://api.anthropic.com/api/organizations/${organization}/reset_rate_limits`
);
expect(JSON.parse(calls[2].data!)).toEqual({
program: 'cedar_ember',
grant_id: grant.id,
request_id: 'request-1',
});
for (const call of calls) {
expect(call.authIndex).toBe('index');
expect(call.header?.Authorization).toBe('Bearer $TOKEN$');
expect(call.header?.['anthropic-beta']).toBe('oauth-2025-04-20');
expect(call.header?.['User-Agent']).toBe('claude-cli/2.1.280 (external, cli)');
}
});
test('claim terminal mappings and ambiguous outcomes', async () => {
for (const result of ANTHROPIC_RESET_RESULTS) {
apiCallApi.request = async () => ({
statusCode: 200,
body: { result },
bodyText: '',
header: {},
});
expect(await claimClaudeResetGrant('a', organization, grant.id, 'r')).toBe(result);
}
for (const [statusCode, code] of [
[401, 'auth_error'],
[403, 'auth_error'],
[429, 'rate_limited'],
] as const) {
apiCallApi.request = async () => ({ statusCode, body: null, bodyText: '', header: {} });
expect(await claimClaudeResetGrant('a', organization, grant.id, 'r')).toBe(code);
}
for (const statusCode of [200, 400, 500]) {
apiCallApi.request = async () => ({
statusCode,
body: { result: 'unexpected' },
bodyText: '',
header: {},
});
await expect(claimClaudeResetGrant('a', organization, grant.id, 'r')).rejects.toBeInstanceOf(
AnthropicResetGrantUnknownOutcome
);
}
apiCallApi.request = async () => {
throw new Error('private upstream detail');
};
await expect(claimClaudeResetGrant('a', organization, grant.id, 'r')).rejects.toThrow(
'outcome is unknown'
);
});
function setup() {
let revision = 1;
let now = 100;
let count = 0;
const ids: string[] = [];
const deps = {
revision: () => revision,
now: () => now,
requestId: () => `request-${++count}`,
readStatus: async () => status(),
readOrganization: async () => organization,
claim: async (
_auth: string,
_org: string,
_grant: string,
id: string
): Promise<AnthropicResetSettledCode> => {
ids.push(id);
throw new AnthropicResetGrantUnknownOutcome();
},
};
return {
deps,
ids,
nextSession: () => {
revision++;
},
advance: () => {
now += RETRY_WINDOW_MS;
},
};
}
test('unknown outcome retains request ID across retries; expiry blocks', async () => {
const h = setup();
const operations = createResetGrantOperations(h.deps);
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow();
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow();
expect(h.ids).toEqual(['request-1', 'request-1']);
await expect(operations.run('account', 'a', 'other-grant')).rejects.toThrow('expired');
h.advance();
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow('expired');
expect(h.ids).toHaveLength(2);
});
test('fresh spend rechecks eligibility and blocks concurrent clicks', async () => {
const h = setup();
h.deps.readStatus = async () => ({ ...status(), eligible: false });
const operations = createResetGrantOperations(h.deps);
const first = operations.run('account', 'a', grant.id);
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow('busy');
await expect(first).rejects.toThrow('blocked');
expect(h.ids).toHaveLength(0);
});
test('session change during profile read prevents POST and isolates journal', async () => {
const h = setup();
const operations = createResetGrantOperations(h.deps);
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow();
h.nextSession();
expect(operations.inspect('account')).toBeUndefined();
h.deps.readOrganization = async () => {
h.nextSession();
return organization;
};
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow('session');
expect(h.ids).toHaveLength(1);
});
test('retry binds original organization', async () => {
const h = setup();
const operations = createResetGrantOperations(h.deps);
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow();
h.deps.readOrganization = async () => 'aaaaaaaa-2222-3333-4444-555555555555';
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow('identity');
expect(h.ids).toHaveLength(1);
});
test('a retry refusal keeps the ambiguous original operation open', async () => {
const h = setup();
const operations = createResetGrantOperations(h.deps);
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow();
h.deps.claim = async (_auth, _org, _grant, id) => {
h.ids.push(id);
return 'rate_limited';
};
expect(await operations.run('account', 'a', grant.id)).toEqual({
code: 'rate_limited',
unresolved: true,
});
expect(operations.inspect('account')?.code).toBeUndefined();
h.deps.claim = async (_auth, _org, _grant, id) => {
h.ids.push(id);
return 'already_used';
};
expect(await operations.run('account', 'a', grant.id)).toEqual({
code: 'already_used',
unresolved: false,
});
expect(h.ids).toEqual(['request-1', 'request-1', 'request-1']);
});
test('no retry after the deadline expires during profile lookup', async () => {
const h = setup();
const operations = createResetGrantOperations(h.deps);
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow();
h.deps.readOrganization = async () => {
h.advance();
return organization;
};
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow('expired');
expect(h.ids).toHaveLength(1);
});
test('date, pause, limit, balance, usability and cooldown gates refuse new spending', async () => {
for (const fields of [
{ paused: true },
{ resetsLeft: 0 },
{ usableNow: false },
{ startsAt: '2099-01-01T00:00:00Z' },
{ endsAt: '1970-01-01T00:00:00Z' },
]) {
const h = setup();
h.deps.readStatus = async () => ({
...status(),
grants: [{ ...status().grants[0], ...fields }],
});
await expect(createResetGrantOperations(h.deps).run('account', 'a', grant.id)).rejects.toThrow(
'blocked'
);
expect(h.ids).toHaveLength(0);
}
for (const fields of [{ atLimit: false }, { cooldownUntil: '2099-01-01T00:00:00Z' }]) {
const h = setup();
h.deps.readStatus = async () => ({ ...status(), ...fields });
await expect(createResetGrantOperations(h.deps).run('account', 'a', grant.id)).rejects.toThrow(
'blocked'
);
expect(h.ids).toHaveLength(0);
}
});
test('a stale claim answer cannot settle the replacement session operation', async () => {
const h = setup();
const operations = createResetGrantOperations(h.deps);
h.deps.claim = async () => {
h.nextSession();
expect(operations.inspect('account')).toBeUndefined();
return 'reset';
};
await expect(operations.run('account', 'a', grant.id)).rejects.toThrow('session');
expect(operations.inspect('account')).toBeUndefined();
});
test('all grant messages and confirmation are translated in four locales', async () => {
const locales = await Promise.all(
['en', 'zh-CN', 'zh-TW', 'ru'].map(
async (locale) => (await Bun.file(`src/i18n/locales/${locale}.json`).json()).claude_reset
)
);
for (const locale of locales) {
expect(Object.keys(locale).sort()).toEqual(Object.keys(locales[0]).sort());
expect(locale.confirm_text).toContain('{{grant}}');
expect(locale.count).toContain('{{left}}');
for (const key of [
...ANTHROPIC_RESET_RESULTS,
'auth_error',
'rate_limited',
'unknown',
'expired',
]) {
expect(typeof locale[key]).toBe('string');
}
}
});