@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.
@@ -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,16 +230,32 @@ 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
+ }
@@ -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;
@@ -0,0 +1,288 @@
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 { logger } from "../utils/logger.js";
18
+ import { tokenStore } from "../auth/tokenStore.js";
19
+ import { CLAUDE_CLI_USER_AGENT, OAUTH_BETA_HEADERS, } from "../auth/anthropicOAuth.js";
20
+ import { needsRefresh, refreshTokenFromLatest, isPermanentRefreshFailure, persistTokens, } from "./tokenRefresh.js";
21
+ import { isAccountAllowed } from "./accountSelection.js";
22
+ export const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
23
+ const USAGE_FETCH_TIMEOUT_MS = 10_000;
24
+ /**
25
+ * Enumerate stored Anthropic accounts for a usage refresh.
26
+ * Read-only: no token refresh, no disable side effects — those decisions
27
+ * belong to the caller (fetchAccountUsage / the routing path respectively).
28
+ */
29
+ export async function listAnthropicAccountsForUsage(allowlist) {
30
+ const accounts = [];
31
+ const compoundKeys = await tokenStore.listByPrefix("anthropic:");
32
+ for (const key of compoundKeys) {
33
+ if (!isAccountAllowed(key, allowlist)) {
34
+ continue;
35
+ }
36
+ if (await tokenStore.isDisabled(key)) {
37
+ continue;
38
+ }
39
+ const tokens = await tokenStore.loadTokens(key);
40
+ if (!tokens) {
41
+ continue;
42
+ }
43
+ // Full suffix after the first ":" — a label may itself contain colons,
44
+ // and the quota store and `auth list` rendering key by the full label.
45
+ const separatorIndex = key.indexOf(":");
46
+ accounts.push({
47
+ key,
48
+ label: separatorIndex >= 0 ? key.slice(separatorIndex + 1) : key,
49
+ token: tokens.accessToken,
50
+ refreshToken: tokens.refreshToken,
51
+ expiresAt: tokens.expiresAt,
52
+ type: tokens.tokenType === "Bearer" ? "oauth" : "api_key",
53
+ persistTarget: { providerKey: key },
54
+ });
55
+ }
56
+ return accounts;
57
+ }
58
+ async function requestUsage(token) {
59
+ try {
60
+ const response = await fetch(ANTHROPIC_USAGE_URL, {
61
+ method: "GET",
62
+ headers: {
63
+ authorization: `Bearer ${token}`,
64
+ "anthropic-beta": OAUTH_BETA_HEADERS,
65
+ "user-agent": CLAUDE_CLI_USER_AGENT,
66
+ accept: "application/json",
67
+ },
68
+ signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS),
69
+ });
70
+ return { response };
71
+ }
72
+ catch (err) {
73
+ return { networkError: err instanceof Error ? err.message : String(err) };
74
+ }
75
+ }
76
+ /**
77
+ * Fetch fresh usage windows for one account. Return-not-throw.
78
+ * Refreshes an expiring/expired token first (de-duped, rotation-safe), and
79
+ * retries once through a forced refresh on a 401.
80
+ */
81
+ export async function fetchAccountUsage(account) {
82
+ if (account.type !== "oauth") {
83
+ return {
84
+ ok: false,
85
+ reason: "not_oauth",
86
+ error: "api_key accounts have no subscription usage windows",
87
+ };
88
+ }
89
+ const runRefresh = async () => {
90
+ const result = await refreshTokenFromLatest(account, account.persistTarget);
91
+ if (result.success) {
92
+ if (account.persistTarget) {
93
+ await persistTokens(account.persistTarget, account);
94
+ }
95
+ return null;
96
+ }
97
+ if (isPermanentRefreshFailure(result)) {
98
+ return {
99
+ ok: false,
100
+ reason: "auth",
101
+ error: "reauth required (refresh token rejected)",
102
+ status: result.status,
103
+ };
104
+ }
105
+ const tokenExpired = account.expiresAt
106
+ ? account.expiresAt <= Date.now()
107
+ : false;
108
+ return tokenExpired
109
+ ? {
110
+ ok: false,
111
+ reason: "auth",
112
+ error: "token expired and refresh failed",
113
+ status: result.status,
114
+ }
115
+ : null; // token still valid — attempt the fetch with it
116
+ };
117
+ let refreshedThisCall = false;
118
+ if (needsRefresh(account)) {
119
+ refreshedThisCall = true;
120
+ const failure = await runRefresh();
121
+ if (failure) {
122
+ return failure;
123
+ }
124
+ }
125
+ let { response, networkError } = await requestUsage(account.token);
126
+ if (response?.status === 401 && account.refreshToken && !refreshedThisCall) {
127
+ const failure = await runRefresh();
128
+ if (failure) {
129
+ return failure;
130
+ }
131
+ ({ response, networkError } = await requestUsage(account.token));
132
+ }
133
+ if (!response) {
134
+ return {
135
+ ok: false,
136
+ reason: "network",
137
+ error: networkError ?? "network failure",
138
+ };
139
+ }
140
+ if (!response.ok) {
141
+ const body = await response.text().catch(() => "");
142
+ logger.debug(`[account-usage] usage fetch failed for ${account.label}`, {
143
+ status: response.status,
144
+ body: body.slice(0, 300),
145
+ });
146
+ return {
147
+ ok: false,
148
+ reason: response.status === 401 || response.status === 403 ? "auth" : "http",
149
+ error: `usage endpoint returned HTTP ${response.status}`,
150
+ status: response.status,
151
+ };
152
+ }
153
+ try {
154
+ const usage = (await response.json());
155
+ return { ok: true, usage };
156
+ }
157
+ catch (err) {
158
+ return {
159
+ ok: false,
160
+ reason: "parse",
161
+ error: err instanceof Error ? err.message : String(err),
162
+ };
163
+ }
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // Normalization (pure CPU — unit-testable, no I/O)
167
+ // ---------------------------------------------------------------------------
168
+ const REJECTED_SEVERITIES = new Set([
169
+ "rejected",
170
+ "exceeded",
171
+ "over_limit",
172
+ "blocked",
173
+ ]);
174
+ /**
175
+ * "rejected" iff the window is fully used or the provider severity marks it
176
+ * exhausted; anything unknown stays "allowed". Conservative on purpose: a
177
+ * wrong "rejected" would park an account, a wrong "allowed" just delays
178
+ * parking until the passive capture or the reset. Raw severity is preserved
179
+ * on the window so nothing is lost.
180
+ */
181
+ function deriveWindowStatus(percent, severity) {
182
+ const sev = severity?.trim().toLowerCase();
183
+ if ((typeof percent === "number" && percent >= 100) ||
184
+ (sev !== undefined && REJECTED_SEVERITIES.has(sev))) {
185
+ return "rejected";
186
+ }
187
+ return "allowed";
188
+ }
189
+ /** ISO-8601 → unix seconds; 0 when absent/unparseable (matches headers). */
190
+ function isoToEpochSeconds(value) {
191
+ if (!value) {
192
+ return 0;
193
+ }
194
+ const ms = Date.parse(value);
195
+ return Number.isNaN(ms) ? 0 : Math.floor(ms / 1000);
196
+ }
197
+ function toFraction(percent) {
198
+ return typeof percent === "number" && Number.isFinite(percent)
199
+ ? percent / 100
200
+ : undefined;
201
+ }
202
+ function mapUsageLimit(entry) {
203
+ const window = {
204
+ kind: entry.kind ?? "unknown",
205
+ used: toFraction(entry.percent) ?? 0,
206
+ status: deriveWindowStatus(entry.percent, entry.severity),
207
+ resetsAt: isoToEpochSeconds(entry.resets_at),
208
+ };
209
+ if (entry.group !== undefined) {
210
+ window.group = entry.group;
211
+ }
212
+ if (entry.severity !== null && entry.severity !== undefined) {
213
+ window.severity = entry.severity;
214
+ }
215
+ if (entry.is_active !== null && entry.is_active !== undefined) {
216
+ window.isActive = entry.is_active;
217
+ }
218
+ const scopeModel = entry.scope?.model?.display_name;
219
+ if (scopeModel) {
220
+ window.scopeModel = scopeModel;
221
+ }
222
+ const scopeSurface = entry.scope?.surface;
223
+ if (scopeSurface) {
224
+ window.scopeSurface = scopeSurface;
225
+ }
226
+ return window;
227
+ }
228
+ /**
229
+ * Normalize a usage-endpoint payload into the `AccountQuota` shape used by
230
+ * the passive header path, so a refresh writes through the exact same
231
+ * save/merge/cooldown chain.
232
+ *
233
+ * `prior` supplies fields the usage payload cannot express (fallback and
234
+ * upgrade-path signals) so `isQuotaOverageAvailable` behaves identically
235
+ * before and after a refresh. `unifiedStatus` is deliberately never set:
236
+ * fabricating it would let the reconcile path treat a refresh like a
237
+ * unified-rejected 429.
238
+ */
239
+ export function usageToQuota(usage, opts) {
240
+ const limits = Array.isArray(usage.limits) ? usage.limits : [];
241
+ if (!usage.five_hour && !usage.seven_day && limits.length === 0) {
242
+ return null;
243
+ }
244
+ const { now, prior } = opts;
245
+ const windows = limits.map(mapUsageLimit);
246
+ const sessionLimit = limits.find((entry) => entry.kind === "session");
247
+ const weeklyLimit = limits.find((entry) => entry.kind === "weekly_all");
248
+ const sessionPct = usage.five_hour?.utilization ?? sessionLimit?.percent ?? undefined;
249
+ const weeklyPct = usage.seven_day?.utilization ?? weeklyLimit?.percent ?? undefined;
250
+ // A refresh must never replace a known reset with 0: the payload omits
251
+ // `resets_at` for untouched windows, and a zero reset would stop
252
+ // reconcileCooldownFromQuota from parking a rejected window and make the
253
+ // routing metrics treat it as non-ticking. A stale prior reset degrades
254
+ // gracefully (past resets are ignored / freshened downstream).
255
+ const sessionResetAt = isoToEpochSeconds(usage.five_hour?.resets_at) ||
256
+ isoToEpochSeconds(sessionLimit?.resets_at) ||
257
+ (prior?.sessionResetAt ?? 0);
258
+ const weeklyResetAt = isoToEpochSeconds(usage.seven_day?.resets_at) ||
259
+ isoToEpochSeconds(weeklyLimit?.resets_at) ||
260
+ (prior?.weeklyResetAt ?? 0);
261
+ const overageEnabled = usage.extra_usage?.is_enabled;
262
+ const overageStatus = overageEnabled === true
263
+ ? "allowed"
264
+ : overageEnabled === false
265
+ ? "rejected"
266
+ : (prior?.overageStatus ?? "unknown");
267
+ const quota = {
268
+ sessionUsed: toFraction(sessionPct) ?? prior?.sessionUsed ?? 0,
269
+ sessionStatus: deriveWindowStatus(sessionPct, sessionLimit?.severity),
270
+ sessionResetAt,
271
+ weeklyUsed: toFraction(weeklyPct) ?? prior?.weeklyUsed ?? 0,
272
+ weeklyStatus: deriveWindowStatus(weeklyPct, weeklyLimit?.severity),
273
+ weeklyResetAt,
274
+ fallbackPercentage: prior?.fallbackPercentage ?? 0,
275
+ overageStatus,
276
+ lastUpdated: now,
277
+ windows,
278
+ windowsUpdatedAt: now,
279
+ source: "usage-api",
280
+ };
281
+ if (prior?.fallbackStatus !== undefined) {
282
+ quota.fallbackStatus = prior.fallbackStatus;
283
+ }
284
+ if (prior?.upgradePaths !== undefined) {
285
+ quota.upgradePaths = prior.upgradePaths;
286
+ }
287
+ return quota;
288
+ }
@@ -12,7 +12,7 @@
12
12
  import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
13
13
  import { ProxyTracer } from "../../proxy/proxyTracer.js";
14
14
  import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
15
- import type { AccountAllowlist, AccountAdmissionLease, AccountCooldownPlan, AccountQuota, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyQuotaCooldownUpdate, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
15
+ import type { AccountAllowlist, AccountAdmissionLease, AccountCooldownPlan, AccountQuota, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
16
16
  declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined): AccountAdmissionLease | undefined;
17
17
  declare function enqueueAccountAdmission(accountKey: string, capacity: number): QueuedAccountAdmission;
18
18
  declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<AccountAdmissionLease | undefined>;
@@ -74,6 +74,18 @@ declare function reconcileCooldownFromQuota(state: RuntimeAccountState, quota: A
74
74
  * gracefully because past reset timestamps are ignored by resetEpochToMs.
75
75
  */
76
76
  declare function seedRuntimeQuotasFromDisk(accounts: ProxyPassthroughAccount[]): Promise<void>;
77
+ /**
78
+ * Fetch fresh limits from Anthropic's usage endpoint for every eligible OAuth
79
+ * account and write them through the exact same chain the passive header
80
+ * capture uses (runtime state → cooldown reconciliation → debounced disk
81
+ * snapshot), so routing and `auth list` see the refreshed windows and the
82
+ * automatic path keeps working unchanged on top of them.
83
+ */
84
+ declare function refreshAccountLimits(options?: {
85
+ accountAllowlist?: AccountAllowlist;
86
+ accountFilter?: string;
87
+ snapshotOnly?: boolean;
88
+ }): Promise<ProxyLimitsRefreshResponse>;
77
89
  /**
78
90
  * Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
79
91
  * spend the overall weekly allowance that expires SOONEST first, so quota
@@ -345,6 +357,8 @@ export declare const __testHooks: {
345
357
  maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
346
358
  planCooldownFor429: typeof planCooldownFor429;
347
359
  reconcileCooldownFromQuota: typeof reconcileCooldownFromQuota;
360
+ refreshAccountLimits: typeof refreshAccountLimits;
361
+ clearLimitsRefreshStateForTests: () => void;
348
362
  isRetryableNetworkError: typeof isRetryableNetworkError;
349
363
  isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
350
364
  getStreamFailureDetails: typeof getStreamFailureDetails;