@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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +379 -379
- package/dist/cli/commands/auth.d.ts +8 -1
- package/dist/cli/commands/auth.js +185 -6
- package/dist/cli/factories/authCommandFactory.js +7 -1
- package/dist/lib/processors/archive/ArchiveProcessor.js +45 -24
- package/dist/lib/proxy/accountQuota.d.ts +6 -0
- package/dist/lib/proxy/accountQuota.js +19 -2
- package/dist/lib/proxy/accountUsage.d.ts +45 -0
- package/dist/lib/proxy/accountUsage.js +289 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +15 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +166 -0
- package/dist/lib/types/cli.d.ts +12 -0
- package/dist/lib/types/proxy.d.ts +101 -0
- package/dist/processors/archive/ArchiveProcessor.js +45 -24
- package/dist/proxy/accountQuota.d.ts +6 -0
- package/dist/proxy/accountQuota.js +19 -2
- package/dist/proxy/accountUsage.d.ts +45 -0
- package/dist/proxy/accountUsage.js +288 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +15 -1
- package/dist/server/routes/claudeProxyRoutes.js +166 -0
- package/dist/types/cli.d.ts +12 -0
- package/dist/types/proxy.d.ts +101 -0
- package/package.json +3 -2
|
@@ -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 {
|
|
379
|
+
return {
|
|
380
|
+
key,
|
|
381
|
+
provider,
|
|
382
|
+
label,
|
|
383
|
+
email,
|
|
384
|
+
tier,
|
|
385
|
+
tokenStatus,
|
|
386
|
+
expiresAt,
|
|
387
|
+
tokenType,
|
|
388
|
+
};
|
|
246
389
|
}));
|
|
247
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
980
|
-
//
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|