@juspay/neurolink 10.11.3 → 10.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@
17
17
  * Currently supports:
18
18
  * - Anthropic (API key + OAuth)
19
19
  */
20
- import type { AuthCommandArgs } from "../../lib/types/index.js";
20
+ import type { AccountQuota, AuthCommandArgs } from "../../lib/types/index.js";
21
21
  /**
22
22
  * Handle the login subcommand
23
23
  * `neurolink auth login <provider>`
@@ -26,6 +26,13 @@ import type { AuthCommandArgs } from "../../lib/types/index.js";
26
26
  * (e.g., "anthropic:alice") to support multi-account pools.
27
27
  */
28
28
  export declare function handleLogin(argv: AuthCommandArgs): Promise<void>;
29
+ /**
30
+ * Format the dynamic per-plan limit windows (model-scoped weeklies such as
31
+ * Fable, plus any future kinds) as extra display lines. `session` and
32
+ * `weekly_all` are omitted — they already render as the SESSION / WEEKLY
33
+ * columns. Exported for the continuous test suite.
34
+ */
35
+ export declare function formatQuotaWindowRows(quota: AccountQuota): string[];
29
36
  /**
30
37
  * Handle the list subcommand
31
38
  * `neurolink auth list`
@@ -27,7 +27,8 @@ import ora from "ora";
27
27
  import { logger } from "../../lib/utils/logger.js";
28
28
  import { defaultTokenStore } from "../../lib/auth/tokenStore.js";
29
29
  import { CLAUDE_CODE_CLIENT_ID, ANTHROPIC_AUTH_URL, ANTHROPIC_TOKEN_URL, ANTHROPIC_REDIRECT_URI, CLAUDE_CLI_USER_AGENT, OAUTH_BETA_HEADERS, } from "../../lib/auth/anthropicOAuth.js";
30
- import { loadAccountQuotas } from "../../lib/proxy/accountQuota.js";
30
+ import { flushAccountQuotas, loadAccountQuotas, saveAccountQuota, } from "../../lib/proxy/accountQuota.js";
31
+ import { fetchAccountUsage, listAnthropicAccountsForUsage, usageToQuota, } from "../../lib/proxy/accountUsage.js";
31
32
  // =============================================================================
32
33
  // CONSTANTS
33
34
  // =============================================================================
@@ -161,6 +162,137 @@ function formatQuotaColumns(quota) {
161
162
  : "",
162
163
  };
163
164
  }
165
+ /**
166
+ * Format the dynamic per-plan limit windows (model-scoped weeklies such as
167
+ * Fable, plus any future kinds) as extra display lines. `session` and
168
+ * `weekly_all` are omitted — they already render as the SESSION / WEEKLY
169
+ * columns. Exported for the continuous test suite.
170
+ */
171
+ export function formatQuotaWindowRows(quota) {
172
+ if (!quota.windows?.length) {
173
+ return [];
174
+ }
175
+ const colorize = (pct, text) => {
176
+ if (pct <= 10) {
177
+ return chalk.red(text);
178
+ }
179
+ if (pct <= 30) {
180
+ return chalk.yellow(text);
181
+ }
182
+ return chalk.green(text);
183
+ };
184
+ const rows = [];
185
+ for (const window of quota.windows) {
186
+ if (window.kind === "session" || window.kind === "weekly_all") {
187
+ continue;
188
+ }
189
+ const remaining = Math.round((1 - window.used) * 100);
190
+ const label = window.scopeModel
191
+ ? `${window.group ?? window.kind} (${window.scopeModel})`
192
+ : window.kind;
193
+ const reset = window.resetsAt > 0
194
+ ? chalk.gray(` resets ${formatTimeUntil(window.resetsAt)}`)
195
+ : "";
196
+ rows.push(`${chalk.gray(`${label}:`)} ${colorize(remaining, `${remaining}% left`)}${reset}`);
197
+ }
198
+ return rows;
199
+ }
200
+ /**
201
+ * Fetch fresh limits for `auth list --refresh`.
202
+ *
203
+ * Prefers the running proxy's GET /limits endpoint so the proxy's in-memory
204
+ * routing state is refreshed as a side effect; falls back to fetching the
205
+ * usage endpoint directly from this process (persisting through the same
206
+ * quota store) when no proxy is running or the call fails.
207
+ */
208
+ async function refreshAccountLimitsForList() {
209
+ const errors = [];
210
+ const proxyState = detectRunningProxyState();
211
+ if (proxyState?.port) {
212
+ const host = proxyState.host && proxyState.host !== "0.0.0.0"
213
+ ? proxyState.host
214
+ : "127.0.0.1";
215
+ try {
216
+ const response = await fetch(`http://${host}:${proxyState.port}/limits`, {
217
+ signal: AbortSignal.timeout(45_000),
218
+ });
219
+ if (response.ok) {
220
+ const payload = (await response.json());
221
+ const quotas = {};
222
+ for (const result of payload.results) {
223
+ if (result.quota) {
224
+ quotas[result.account] = result.quota;
225
+ }
226
+ if (result.status === "error" && result.error) {
227
+ errors.push(`${result.account}: ${result.error}`);
228
+ }
229
+ }
230
+ return { via: "proxy", quotas, errors };
231
+ }
232
+ errors.push(`running proxy /limits returned HTTP ${response.status}; fetching directly`);
233
+ }
234
+ catch {
235
+ // Keep the message generic: a raw fetch error can echo the requested
236
+ // URL, and this string reaches the text and JSON CLI output.
237
+ errors.push("running proxy /limits unreachable; fetching directly");
238
+ }
239
+ }
240
+ try {
241
+ const accounts = await listAnthropicAccountsForUsage();
242
+ const prior = await loadAccountQuotas().catch(() => ({}));
243
+ const quotas = {};
244
+ const CONCURRENCY = 3;
245
+ let nextIndex = 0;
246
+ const worker = async () => {
247
+ for (;;) {
248
+ const index = nextIndex++;
249
+ if (index >= accounts.length) {
250
+ return;
251
+ }
252
+ const account = accounts[index];
253
+ if (account.type !== "oauth") {
254
+ continue; // api_key accounts have no subscription windows
255
+ }
256
+ // Isolate failures per account: one rejection must not abort the
257
+ // Promise.all sweep or discard the other accounts' refreshed quotas.
258
+ try {
259
+ const result = await fetchAccountUsage(account);
260
+ if (!result.ok) {
261
+ errors.push(`${account.label}: ${result.error}`);
262
+ continue;
263
+ }
264
+ const quota = usageToQuota(result.usage, {
265
+ now: Date.now(),
266
+ prior: prior[account.label] ?? null,
267
+ });
268
+ if (!quota) {
269
+ errors.push(`${account.label}: usage payload had no recognizable limit windows`);
270
+ continue;
271
+ }
272
+ await saveAccountQuota(account.label, quota);
273
+ quotas[account.label] = quota;
274
+ }
275
+ catch (err) {
276
+ errors.push(`${account.label}: ${err instanceof Error ? err.message : String(err)}`);
277
+ }
278
+ }
279
+ };
280
+ try {
281
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, accounts.length || 1) }, () => worker()));
282
+ }
283
+ finally {
284
+ // The quota store's debounced flush timer is unref()'d and this process
285
+ // is short-lived — flush now (even on a partial sweep) or the completed
286
+ // saves never reach disk.
287
+ await flushAccountQuotas().catch(() => undefined);
288
+ }
289
+ return { via: "direct", quotas, errors };
290
+ }
291
+ catch (err) {
292
+ errors.push(`direct limit fetch failed (${err instanceof Error ? err.message : String(err)})`);
293
+ return { via: "none", quotas: null, errors };
294
+ }
295
+ }
164
296
  /**
165
297
  * Handle the list subcommand
166
298
  * `neurolink auth list`
@@ -189,6 +321,7 @@ export async function handleList(argv) {
189
321
  let email;
190
322
  let tokenStatus = "unknown";
191
323
  let expiresAt;
324
+ let tokenType;
192
325
  // Derive email from the compound key label when it looks like an email.
193
326
  // The credentials file is a shared singleton that gets overwritten on
194
327
  // every login — reading email from it would show the LATEST login's
@@ -220,6 +353,7 @@ export async function handleList(argv) {
220
353
  const tokens = await defaultTokenStore.loadTokens(key);
221
354
  if (tokens) {
222
355
  expiresAt = tokens.expiresAt;
356
+ tokenType = tokens.tokenType;
223
357
  const isExpired = defaultTokenStore.isTokenExpired(tokens, 0);
224
358
  tokenStatus = isExpired ? "expired" : "valid";
225
359
  // Extract per-account metadata from scope (e.g. "tier:pro email:user@example.com")
@@ -242,9 +376,24 @@ export async function handleList(argv) {
242
376
  catch {
243
377
  // Token load failed — show as unknown
244
378
  }
245
- return { key, provider, label, email, tier, tokenStatus, expiresAt };
379
+ return {
380
+ key,
381
+ provider,
382
+ label,
383
+ email,
384
+ tier,
385
+ tokenStatus,
386
+ expiresAt,
387
+ tokenType,
388
+ };
246
389
  }));
247
- // Load persisted quota data (captured from proxy responses).
390
+ // Optionally fetch FRESH limits from Anthropic before rendering.
391
+ let refreshOutcome;
392
+ if (argv.refresh) {
393
+ refreshOutcome = await refreshAccountLimitsForList();
394
+ }
395
+ // Load persisted quota data (captured from proxy responses), then overlay
396
+ // anything just refreshed — freshly fetched values win over the snapshot.
248
397
  let quotas = {};
249
398
  try {
250
399
  quotas = await loadAccountQuotas();
@@ -252,6 +401,9 @@ export async function handleList(argv) {
252
401
  catch {
253
402
  // Non-fatal — quota display is best-effort
254
403
  }
404
+ if (refreshOutcome?.quotas) {
405
+ quotas = { ...quotas, ...refreshOutcome.quotas };
406
+ }
255
407
  if (argv.format === "json") {
256
408
  // Merge quota data into each account object for JSON output
257
409
  const withQuota = enrichedAccounts.map((acct) => {
@@ -259,9 +411,29 @@ export async function handleList(argv) {
259
411
  const quota = quotas[quotaKey] ?? null;
260
412
  return { ...acct, quota };
261
413
  });
262
- logger.always(JSON.stringify(withQuota, null, 2));
414
+ if (refreshOutcome) {
415
+ // --refresh envelopes the array so the fetch outcome travels with it.
416
+ logger.always(JSON.stringify({
417
+ refresh: {
418
+ via: refreshOutcome.via,
419
+ errors: refreshOutcome.errors,
420
+ },
421
+ accounts: withQuota,
422
+ }, null, 2));
423
+ }
424
+ else {
425
+ logger.always(JSON.stringify(withQuota, null, 2));
426
+ }
263
427
  }
264
428
  else {
429
+ if (refreshOutcome) {
430
+ if (refreshOutcome.via !== "none") {
431
+ logger.always(chalk.gray(`\nFetched fresh limits from Anthropic (${refreshOutcome.via === "proxy" ? "via running proxy" : "direct"}).`));
432
+ }
433
+ for (const refreshError of refreshOutcome.errors) {
434
+ logger.always(chalk.yellow(`⚠ ${refreshError}`));
435
+ }
436
+ }
265
437
  logger.always(chalk.bold("\nAuthenticated Accounts:\n"));
266
438
  // Check if any account has quota data to decide column layout
267
439
  const hasQuota = enrichedAccounts.some((acct) => {
@@ -296,14 +468,21 @@ export async function handleList(argv) {
296
468
  if (hasQuota && quota) {
297
469
  const qc = formatQuotaColumns(quota);
298
470
  logger.always(` ${chalk.cyan(displayLabel)} ${displayProvider} ${displayEmail} ${statusText} ${qc.sessionText.padEnd(10)} ${qc.weeklyText.padEnd(10)}`);
471
+ const indent = " ".repeat(2 + 20 + 1 + 12 + 1 + 28 + 1 + 14 + 1);
299
472
  // Second line: reset times (indented under session/weekly columns)
300
473
  if (qc.sessionReset || qc.weeklyReset) {
301
- const indent = " ".repeat(2 + 20 + 1 + 12 + 1 + 28 + 1 + 14 + 1);
302
474
  logger.always(`${indent}${(qc.sessionReset || "").padEnd(10)} ${qc.weeklyReset || ""}`);
303
475
  }
476
+ // Dynamic per-plan windows (e.g. the Fable-only weekly limit)
477
+ for (const windowRow of formatQuotaWindowRows(quota)) {
478
+ logger.always(`${indent}${windowRow}`);
479
+ }
304
480
  }
305
481
  else {
306
- logger.always(` ${chalk.cyan(displayLabel)} ${displayProvider} ${displayEmail} ${statusText}${hasQuota ? " - -" : ""}`);
482
+ const apiKeyNote = refreshOutcome && acct.tokenType && acct.tokenType !== "Bearer"
483
+ ? chalk.gray(" (api key — not refreshed)")
484
+ : "";
485
+ logger.always(` ${chalk.cyan(displayLabel)} ${displayProvider} ${displayEmail} ${statusText}${hasQuota ? " - -" : ""}${apiKeyNote}`);
307
486
  }
308
487
  }
309
488
  logger.always("");
@@ -214,8 +214,14 @@ export class AuthCommandFactory {
214
214
  */
215
215
  static buildListOptions(yargs) {
216
216
  return yargs
217
+ .option("refresh", {
218
+ type: "boolean",
219
+ default: false,
220
+ description: "Fetch fresh limits from Anthropic for all OAuth accounts before listing (via the running proxy when available)",
221
+ })
217
222
  .example("$0 auth list", "List all authenticated accounts")
218
- .example("$0 auth list --format json", "List accounts in JSON format");
223
+ .example("$0 auth list --format json", "List accounts in JSON format")
224
+ .example("$0 auth list --refresh", "Fetch fresh session/weekly/model-scoped limits before listing");
219
225
  }
220
226
  /**
221
227
  * Build options for remove subcommand
@@ -162,6 +162,17 @@ const SINGLE_STREAM_TOOLS = {
162
162
  xz: "xz",
163
163
  zst: "zstd",
164
164
  };
165
+ /**
166
+ * Whether a zlib rejection is the output bound firing rather than bad input.
167
+ *
168
+ * `maxOutputLength` aborts an inflate the moment its output would pass the cap,
169
+ * which is the whole point — but it surfaces as a plain `RangeError`, and a
170
+ * bomb reported as "failed to decompress" reads as a corrupt upload and invites
171
+ * the user to send it again. It will fail identically every time.
172
+ *
173
+ * Keyed on `code`, not the message: the message embeds a byte count.
174
+ */
175
+ const isDecompressionBoundExceeded = (error) => error?.code === "ERR_BUFFER_TOO_LARGE";
165
176
  /** File extensions recognized as archive formats */
166
177
  const SUPPORTED_ARCHIVE_EXTENSIONS = [".zip", ".tar", ".gz", ".tgz", ".bz2", ".tbz2", ".jar", ".xz", ".txz", ".zst", ".tzst"];
167
178
  // =============================================================================
@@ -761,19 +772,16 @@ export class ArchiveProcessor extends BaseFileProcessor {
761
772
  const zlib = await import("zlib");
762
773
  const { promisify } = await import("util");
763
774
  const gunzip = promisify(zlib.gunzip);
764
- const decompressed = await gunzip(buffer);
775
+ // Bounded at the decoder, matching the zstd path. Checking the length
776
+ // afterwards only reports a bomb once it has already been paid for: 40KB
777
+ // of gzip inflates to 40MB, and the allocation is the damage, not the
778
+ // number. `maxOutputLength` abandons the inflate at the cap instead, so
779
+ // the ceiling on memory is the limit rather than whatever the attacker
780
+ // chose. The overflow is classified in the catch below.
781
+ const decompressed = await gunzip(buffer, {
782
+ maxOutputLength: ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE,
783
+ });
765
784
  const tarBuffer = Buffer.from(decompressed);
766
- // Security: check decompressed size
767
- if (tarBuffer.length > ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE) {
768
- return {
769
- success: false,
770
- entries: [],
771
- securityWarnings: [],
772
- error: this.createError(FileErrorCode.SECURITY_VALIDATION_FAILED, {
773
- reason: `Decompressed TAR size (${this.formatSizeMB(tarBuffer.length)} MB) exceeds limit (${this.formatSizeMB(ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE)} MB)`,
774
- }),
775
- };
776
- }
777
785
  // Security: check compression ratio
778
786
  if (buffer.length > 0) {
779
787
  const ratio = tarBuffer.length / buffer.length;
@@ -793,6 +801,16 @@ export class ArchiveProcessor extends BaseFileProcessor {
793
801
  return await this.parseTarStream(tarStream, tarBuffer);
794
802
  }
795
803
  catch (error) {
804
+ if (isDecompressionBoundExceeded(error)) {
805
+ return {
806
+ success: false,
807
+ entries: [],
808
+ securityWarnings: [],
809
+ error: this.createError(FileErrorCode.SECURITY_VALIDATION_FAILED, {
810
+ reason: `Decompressed TAR size exceeds limit (${this.formatSizeMB(ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE)} MB)`,
811
+ }),
812
+ };
813
+ }
796
814
  // Check if the error is one we already created (security validation)
797
815
  if (error &&
798
816
  typeof error === "object" &&
@@ -976,18 +994,11 @@ export class ArchiveProcessor extends BaseFileProcessor {
976
994
  const zlib = await import("zlib");
977
995
  const { promisify } = await import("util");
978
996
  const gunzip = promisify(zlib.gunzip);
979
- const decompressed = await gunzip(buffer);
980
- // Security: check decompressed size
981
- if (decompressed.length > ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE) {
982
- return {
983
- success: false,
984
- entries: [],
985
- securityWarnings: [],
986
- error: this.createError(FileErrorCode.SECURITY_VALIDATION_FAILED, {
987
- reason: `Decompressed size (${this.formatSizeMB(decompressed.length)} MB) exceeds limit (${this.formatSizeMB(ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE)} MB)`,
988
- }),
989
- };
990
- }
997
+ // Bounded at the decoder — see the matching call in extractTarGzEntries.
998
+ // The overflow is classified in the catch below.
999
+ const decompressed = await gunzip(buffer, {
1000
+ maxOutputLength: ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE,
1001
+ });
991
1002
  // Security: compression ratio
992
1003
  if (buffer.length > 0) {
993
1004
  const ratio = decompressed.length / buffer.length;
@@ -1031,6 +1042,16 @@ export class ArchiveProcessor extends BaseFileProcessor {
1031
1042
  return { success: true, entries, securityWarnings, contents };
1032
1043
  }
1033
1044
  catch (error) {
1045
+ if (isDecompressionBoundExceeded(error)) {
1046
+ return {
1047
+ success: false,
1048
+ entries: [],
1049
+ securityWarnings: [],
1050
+ error: this.createError(FileErrorCode.SECURITY_VALIDATION_FAILED, {
1051
+ reason: `Decompressed size exceeds limit (${this.formatSizeMB(ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE)} MB)`,
1052
+ }),
1053
+ };
1054
+ }
1034
1055
  return {
1035
1056
  success: false,
1036
1057
  entries: [],
@@ -48,4 +48,10 @@ export declare function loadAccountQuota(accountKey: string): Promise<AccountQuo
48
48
  * other accounts' snapshots and blinded quota-aware routing to them).
49
49
  */
50
50
  export declare function saveAccountQuota(accountKey: string, quota: AccountQuota): Promise<void>;
51
+ /**
52
+ * Cancel any pending debounced write and flush the cache to disk now.
53
+ * Short-lived processes (CLI refresh path) must call this before exit —
54
+ * the debounce timer is unref()'d and will not keep the process alive.
55
+ */
56
+ export declare function flushAccountQuotas(): Promise<void>;
51
57
  export declare function flushAccountQuotaStateForTests(): Promise<void>;
@@ -94,6 +94,7 @@ export function parseQuotaHeaders(headers) {
94
94
  upgradePaths: getHeader(headers, `${P}unified-upgrade-paths`),
95
95
  overageStatus: getHeader(headers, `${P}unified-overage-status`) ?? "unknown",
96
96
  lastUpdated: Date.now(),
97
+ source: "headers",
97
98
  };
98
99
  }
99
100
  // ---------------------------------------------------------------------------
@@ -229,17 +230,33 @@ export async function loadAccountQuota(accountKey) {
229
230
  export async function saveAccountQuota(accountKey, quota) {
230
231
  await stateMutex.runExclusive(async () => {
231
232
  await ensureAccountQuotasLoaded();
232
- memoryCache[accountKey] = { ...quota };
233
+ const next = { ...quota };
234
+ // Header-sourced saves carry no dynamic windows; a passive capture right
235
+ // after a usage-API refresh must not erase the refreshed buckets.
236
+ const existing = memoryCache[accountKey];
237
+ if (next.windows === undefined && existing?.windows !== undefined) {
238
+ next.windows = existing.windows;
239
+ next.windowsUpdatedAt = existing.windowsUpdatedAt;
240
+ }
241
+ memoryCache[accountKey] = next;
233
242
  dirty = true;
234
243
  cacheVersion += 1;
235
244
  });
236
245
  scheduleFlush();
237
246
  }
238
- export async function flushAccountQuotaStateForTests() {
247
+ /**
248
+ * Cancel any pending debounced write and flush the cache to disk now.
249
+ * Short-lived processes (CLI refresh path) must call this before exit —
250
+ * the debounce timer is unref()'d and will not keep the process alive.
251
+ */
252
+ export async function flushAccountQuotas() {
239
253
  if (flushTimer) {
240
254
  clearTimeout(flushTimer);
241
255
  flushTimer = null;
242
256
  }
243
257
  await flushToDisk();
244
258
  }
259
+ export async function flushAccountQuotaStateForTests() {
260
+ await flushAccountQuotas();
261
+ }
245
262
  //# sourceMappingURL=accountQuota.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * On-Demand Account Usage Fetch
3
+ *
4
+ * Manual refetch path for account limits: queries Anthropic's OAuth usage
5
+ * endpoint (the same call Claude Code's /usage makes) to get FRESH session /
6
+ * weekly / model-scoped windows for an account without consuming any tokens
7
+ * and without starting a 5h session window.
8
+ *
9
+ * This complements — never replaces — the passive header capture in
10
+ * accountQuota.ts: `usageToQuota` normalizes the endpoint payload into the
11
+ * same `AccountQuota` shape so refreshed data flows through the existing
12
+ * save/merge/cooldown chain.
13
+ *
14
+ * Only OAuth (Bearer) accounts have subscription windows; api_key accounts
15
+ * are skipped and keep their header-derived absolute limits.
16
+ */
17
+ import type { AccountAllowlist, AccountQuota, AccountUsageFetchResult, AnthropicUsageResponse, ProxyPassthroughAccount } from "../types/index.js";
18
+ export declare const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
19
+ /**
20
+ * Enumerate stored Anthropic accounts for a usage refresh.
21
+ * Read-only: no token refresh, no disable side effects — those decisions
22
+ * belong to the caller (fetchAccountUsage / the routing path respectively).
23
+ */
24
+ export declare function listAnthropicAccountsForUsage(allowlist?: AccountAllowlist): Promise<ProxyPassthroughAccount[]>;
25
+ /**
26
+ * Fetch fresh usage windows for one account. Return-not-throw.
27
+ * Refreshes an expiring/expired token first (de-duped, rotation-safe), and
28
+ * retries once through a forced refresh on a 401.
29
+ */
30
+ export declare function fetchAccountUsage(account: ProxyPassthroughAccount): Promise<AccountUsageFetchResult>;
31
+ /**
32
+ * Normalize a usage-endpoint payload into the `AccountQuota` shape used by
33
+ * the passive header path, so a refresh writes through the exact same
34
+ * save/merge/cooldown chain.
35
+ *
36
+ * `prior` supplies fields the usage payload cannot express (fallback and
37
+ * upgrade-path signals) so `isQuotaOverageAvailable` behaves identically
38
+ * before and after a refresh. `unifiedStatus` is deliberately never set:
39
+ * fabricating it would let the reconcile path treat a refresh like a
40
+ * unified-rejected 429.
41
+ */
42
+ export declare function usageToQuota(usage: AnthropicUsageResponse, opts: {
43
+ now: number;
44
+ prior?: AccountQuota | null;
45
+ }): AccountQuota | null;