@juspay/neurolink 10.1.2 → 10.2.0

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.
@@ -41,6 +41,8 @@ const _require = createRequire(import.meta.url);
41
41
  const PROXY_VERSION = packageJson.version;
42
42
  const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/observability/manage-local-openobserve.sh", import.meta.url));
43
43
  const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
44
+ const LEGACY_STATUS_ACCOUNT_CACHE_TTL_MS = 5_000;
45
+ let legacyStatusAccountCache;
44
46
  // Allowed drift between a pid's OS-reported start time and the persisted
45
47
  // ProxySupervisorState.startTime before processLooksLikeProxySupervisor
46
48
  // treats it as a confident mismatch (recycled pid). Generous on purpose:
@@ -323,6 +325,34 @@ async function resolveStatusPrimaryAccount(proxyConfig) {
323
325
  source: "fallback",
324
326
  };
325
327
  }
328
+ async function resolveLegacyStatusAccountLabel(storedAnthropicAccountCount) {
329
+ if (storedAnthropicAccountCount !== 0) {
330
+ return null;
331
+ }
332
+ const credentialsPath = join(homedir(), ".neurolink", "anthropic-credentials.json");
333
+ const now = Date.now();
334
+ if (legacyStatusAccountCache?.credentialsPath === credentialsPath &&
335
+ legacyStatusAccountCache.expiresAt > now) {
336
+ return legacyStatusAccountCache.label;
337
+ }
338
+ let label = null;
339
+ try {
340
+ const { readFile } = await import("node:fs/promises");
341
+ const parsed = JSON.parse(await readFile(credentialsPath, "utf8"));
342
+ if (parsed.oauth?.accessToken) {
343
+ label = parsed.email?.trim() || "legacy-default";
344
+ }
345
+ }
346
+ catch {
347
+ label = null;
348
+ }
349
+ legacyStatusAccountCache = {
350
+ credentialsPath,
351
+ expiresAt: now + LEGACY_STATUS_ACCOUNT_CACHE_TTL_MS,
352
+ label,
353
+ };
354
+ return label;
355
+ }
326
356
  /**
327
357
  * Check if the launchd service is loaded and actively managing the proxy.
328
358
  * Returns true if launchctl reports the service as running.
@@ -1492,23 +1522,32 @@ export async function createProxyStartApp(params) {
1492
1522
  const activeAccountAllowlist = runtimeConfig
1493
1523
  ? runtimeConfig.accountAllowlist
1494
1524
  : params.accountAllowlist;
1495
- const { getStats } = await import("../../lib/proxy/usageStats.js");
1525
+ const { getReconciledStats, getUsageStatsPersistenceStatus } = await import("../../lib/proxy/usageStats.js");
1496
1526
  const { loadAccountCooldowns } = await import("../../lib/proxy/accountCooldown.js");
1497
- const stats = getStats();
1527
+ const stats = await getReconciledStats();
1498
1528
  const runtimeState = loadProxyState();
1499
1529
  const supervisorState = loadProxySupervisorState();
1500
1530
  const updateState = loadUpdateState();
1501
1531
  const cooldowns = await loadAccountCooldowns();
1502
1532
  const storedAccountKeys = new Set();
1533
+ const disabledAccountKeys = new Set();
1534
+ let accountInventoryLoaded = false;
1503
1535
  try {
1504
1536
  const { tokenStore } = await import("../../lib/auth/tokenStore.js");
1505
1537
  for (const key of await tokenStore.listByPrefix("anthropic:")) {
1506
1538
  storedAccountKeys.add(normalizeAnthropicAccountKey(key));
1507
1539
  }
1540
+ for (const key of await tokenStore.listDisabled()) {
1541
+ disabledAccountKeys.add(normalizeAnthropicAccountKey(key));
1542
+ }
1543
+ accountInventoryLoaded = true;
1508
1544
  }
1509
1545
  catch (err) {
1510
1546
  logger.debug(`[proxy] /status: failed to resolve account cooldown labels: ${err instanceof Error ? err.message : String(err)}`);
1511
1547
  }
1548
+ const legacyAccountLabel = accountInventoryLoaded
1549
+ ? await resolveLegacyStatusAccountLabel(storedAccountKeys.size)
1550
+ : null;
1512
1551
  const now = Date.now();
1513
1552
  const health = buildProxyHealthResponse(readiness, {
1514
1553
  strategy: activeStrategy,
@@ -1530,6 +1569,7 @@ export async function createProxyStartApp(params) {
1530
1569
  version: PROXY_VERSION,
1531
1570
  health,
1532
1571
  stats: {
1572
+ startedAt: stats.startedAt,
1533
1573
  totalAttempts: stats.totalAttempts,
1534
1574
  totalAttemptErrors: stats.totalAttemptErrors,
1535
1575
  totalRequests: stats.totalRequests,
@@ -1540,13 +1580,25 @@ export async function createProxyStartApp(params) {
1540
1580
  totalQuotaRateLimits: stats.totalQuotaRateLimits,
1541
1581
  accounts: Object.values(stats.accounts).map((account) => {
1542
1582
  const normalizedKey = normalizeAnthropicAccountKey(account.label);
1583
+ const isLegacyAccount = account.type === "oauth" && account.label === legacyAccountLabel;
1543
1584
  const accountKey = storedAccountKeys.has(normalizedKey)
1544
1585
  ? normalizedKey
1545
- : account.label === "env"
1546
- ? ENV_ANTHROPIC_ACCOUNT_KEY
1547
- : account.type === "oauth"
1548
- ? LEGACY_ANTHROPIC_ACCOUNT_KEY
1586
+ : isLegacyAccount
1587
+ ? LEGACY_ANTHROPIC_ACCOUNT_KEY
1588
+ : account.label === "env"
1589
+ ? ENV_ANTHROPIC_ACCOUNT_KEY
1549
1590
  : normalizedKey;
1591
+ const isStored = storedAccountKeys.has(accountKey) || isLegacyAccount;
1592
+ const cooling = (cooldowns[accountKey]?.coolingUntil ?? 0) > now;
1593
+ const accountStatus = disabledAccountKeys.has(accountKey)
1594
+ ? "disabled"
1595
+ : !isAccountAllowed(accountKey, activeAccountAllowlist)
1596
+ ? "excluded"
1597
+ : accountInventoryLoaded && account.type === "oauth" && !isStored
1598
+ ? "removed"
1599
+ : cooling
1600
+ ? "cooling"
1601
+ : "active";
1550
1602
  return {
1551
1603
  label: account.label,
1552
1604
  type: account.type,
@@ -1558,10 +1610,12 @@ export async function createProxyStartApp(params) {
1558
1610
  rateLimits: account.rateLimitCount,
1559
1611
  transientRateLimits: account.transientRateLimitCount,
1560
1612
  quotaRateLimits: account.quotaRateLimitCount,
1561
- cooling: (cooldowns[accountKey]?.coolingUntil ?? 0) > now,
1613
+ cooling,
1614
+ status: accountStatus,
1562
1615
  };
1563
1616
  }),
1564
1617
  primaryAccount,
1618
+ persistence: getUsageStatsPersistenceStatus(),
1565
1619
  },
1566
1620
  activity: (() => {
1567
1621
  const activity = getProxyActivitySnapshot();
@@ -1875,10 +1929,17 @@ function registerProxyShutdownHandlers(params) {
1875
1929
  logger.error(`[proxy] failed to drain server during shutdown: ${error instanceof Error ? error.message : String(error)}`);
1876
1930
  }
1877
1931
  }
1878
- try {
1879
- await withTimeout(flushProxyLifecycleEvents(), PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "Timed out flushing proxy lifecycle metadata during shutdown");
1932
+ const usageStatsFlush = import("../../lib/proxy/usageStats.js").then(({ flushUsageStats }) => flushUsageStats());
1933
+ const [usageStatsFlushResult, lifecycleFlushResult] = await Promise.allSettled([
1934
+ withTimeout(usageStatsFlush, PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "Timed out flushing proxy usage statistics during shutdown"),
1935
+ withTimeout(flushProxyLifecycleEvents(), PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "Timed out flushing proxy lifecycle metadata during shutdown"),
1936
+ ]);
1937
+ if (usageStatsFlushResult.status === "rejected") {
1938
+ const error = usageStatsFlushResult.reason;
1939
+ logger.warn(`[proxy] usage statistics flush failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
1880
1940
  }
1881
- catch (error) {
1941
+ if (lifecycleFlushResult.status === "rejected") {
1942
+ const error = lifecycleFlushResult.reason;
1882
1943
  logger.debug(`[proxy] lifecycle metadata flush failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
1883
1944
  }
1884
1945
  try {
@@ -2284,9 +2345,10 @@ async function startProxyCommandHandler(argv) {
2284
2345
  }
2285
2346
  // In dev mode: redirect writable state to .neurolink-dev/ and skip singleton check
2286
2347
  let devPaths;
2348
+ const { resolveProxyPaths, resolveProxyUsageStatsPath } = await import("../../lib/proxy/proxyPaths.js");
2349
+ const proxyPaths = resolveProxyPaths(isDev);
2287
2350
  if (isDev) {
2288
- const { resolveProxyPaths } = await import("../../lib/proxy/proxyPaths.js");
2289
- devPaths = resolveProxyPaths(true);
2351
+ devPaths = proxyPaths;
2290
2352
  setProxyStateDir(devPaths.stateDir);
2291
2353
  const { initAccountQuota } = await import("../../lib/proxy/accountQuota.js");
2292
2354
  initAccountQuota(devPaths.quotaFile);
@@ -2301,6 +2363,15 @@ async function startProxyCommandHandler(argv) {
2301
2363
  if (!isDev && !socketWorker) {
2302
2364
  await ensureProxyStartAllowed(spinner);
2303
2365
  }
2366
+ const { initUsageStats, getUsageStatsPersistenceStatus } = await import("../../lib/proxy/usageStats.js");
2367
+ await initUsageStats(resolveProxyUsageStatsPath(proxyPaths));
2368
+ const usageStatsPersistence = getUsageStatsPersistenceStatus();
2369
+ if (usageStatsPersistence.lastError) {
2370
+ logger.warn(`[proxy] usage statistics persistence unavailable: ${usageStatsPersistence.lastError}`);
2371
+ }
2372
+ else if (usageStatsPersistence.lastRecoveryAt) {
2373
+ logger.warn(`[proxy] recovered corrupt usage statistics state at ${new Date(usageStatsPersistence.lastRecoveryAt).toISOString()}`);
2374
+ }
2304
2375
  const baseEnv = { ...process.env };
2305
2376
  const envResolution = resolveProxyEnvFile({
2306
2377
  explicitEnvFile: argv.envFile,
@@ -2485,7 +2556,7 @@ function printStatusStats(stats) {
2485
2556
  String(account.success ?? 0),
2486
2557
  String(account.errors ?? 0),
2487
2558
  String(account.rateLimits ?? 0),
2488
- account.cooling ? "cooling" : "active",
2559
+ account.status ?? (account.cooling ? "cooling" : "active"),
2489
2560
  ]);
2490
2561
  const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
2491
2562
  const numericColumns = new Set([2, 3, 4, 5]);
@@ -2503,7 +2574,7 @@ function printStatusStats(stats) {
2503
2574
  const statusStart = formatted.length - widths[6];
2504
2575
  const prefix = formatted.slice(0, statusStart);
2505
2576
  const paddedStatus = formatted.slice(statusStart);
2506
- console.info(` ${chalk.cyan(prefix.slice(0, widths[0]))}${prefix.slice(widths[0])}${status === "cooling" ? chalk.red(paddedStatus) : chalk.green(paddedStatus)}`);
2577
+ console.info(` ${chalk.cyan(prefix.slice(0, widths[0]))}${prefix.slice(widths[0])}${status === "active" ? chalk.green(paddedStatus) : status === "cooling" ? chalk.red(paddedStatus) : chalk.yellow(paddedStatus)}`);
2507
2578
  }
2508
2579
  }
2509
2580
  }
@@ -14,3 +14,4 @@
14
14
  */
15
15
  import type { ProxyPaths } from "../types/index.js";
16
16
  export declare function resolveProxyPaths(dev: boolean): ProxyPaths;
17
+ export declare function resolveProxyUsageStatsPath(paths: ProxyPaths): string;
@@ -22,6 +22,7 @@ export function resolveProxyPaths(dev) {
22
22
  logsDir: join(base, "logs"),
23
23
  quotaFile: join(base, "account-quotas.json"),
24
24
  cooldownFile: join(base, "account-cooldowns.json"),
25
+ statsFile: join(base, "proxy-usage-stats.json"),
25
26
  isDev: true,
26
27
  };
27
28
  }
@@ -31,7 +32,11 @@ export function resolveProxyPaths(dev) {
31
32
  logsDir: join(base, "logs"),
32
33
  quotaFile: join(base, "account-quotas.json"),
33
34
  cooldownFile: join(base, "account-cooldowns.json"),
35
+ statsFile: join(base, "proxy-usage-stats.json"),
34
36
  isDev: false,
35
37
  };
36
38
  }
39
+ export function resolveProxyUsageStatsPath(paths) {
40
+ return paths.statsFile ?? join(paths.stateDir, "proxy-usage-stats.json");
41
+ }
37
42
  //# sourceMappingURL=proxyPaths.js.map
@@ -1,13 +1,67 @@
1
1
  /**
2
- * Proxy Usage Statistics
3
- * Tracks per-account request counts, token usage, and error rates.
4
- * In-memory only resets on proxy restart.
2
+ * Proxy usage statistics with restart- and handoff-safe persistence.
3
+ *
4
+ * Request handlers update memory synchronously. Small deltas are merged into a
5
+ * shared snapshot asynchronously, under a cross-process lock, so overlapping
6
+ * rolling workers cannot overwrite each other's counters.
5
7
  */
6
- import type { AccountStats, ProxyStats } from "../types/index.js";
8
+ import type { AccountStats, ProxyStats, ProxyStatsPersistenceStatus, ProxyUsageStatsStoreOptions } from "../types/index.js";
9
+ export declare class ProxyUsageStatsStore {
10
+ private readonly now;
11
+ private readonly flushIntervalMs;
12
+ private readonly lockTimeoutMs;
13
+ private readonly staleLockMs;
14
+ private filePath?;
15
+ private stats;
16
+ private pending;
17
+ private pendingMutations;
18
+ private inFlightMutations;
19
+ private revision;
20
+ private flushTimer;
21
+ private readonly flushMutex;
22
+ private lastFlushedAt?;
23
+ private lastReconciledAt?;
24
+ private lastRecoveryAt?;
25
+ private lastError?;
26
+ constructor(options?: ProxyUsageStatsStoreOptions);
27
+ initialize(filePath?: string): Promise<void>;
28
+ recordAttempt(accountLabel: string, accountType: string): void;
29
+ recordFinalSuccess(accountLabel?: string, accountType?: string): void;
30
+ recordAttemptError(accountLabel: string, accountType: string, status: number, rateLimitKind?: "transient" | "quota"): void;
31
+ recordFinalError(_status: number, accountLabel?: string, accountType?: string): void;
32
+ getStats(): ProxyStats;
33
+ getAccountStats(label: string): AccountStats | undefined;
34
+ getPersistenceStatus(): ProxyStatsPersistenceStatus;
35
+ flush(): Promise<void>;
36
+ reconcile(): Promise<ProxyStats>;
37
+ resetMemory(): void;
38
+ resetForTests(): Promise<void>;
39
+ private markMutation;
40
+ private applyAttempt;
41
+ private applyFinalSuccess;
42
+ private applyAttemptError;
43
+ private applyFinalError;
44
+ private ensureAccount;
45
+ private scheduleFlush;
46
+ private cancelFlushTimer;
47
+ private readSnapshot;
48
+ private applySnapshot;
49
+ private recoverCorruptSnapshot;
50
+ private quarantineCorruptSnapshot;
51
+ private pruneCorruptSnapshots;
52
+ private describeError;
53
+ }
54
+ export declare function initUsageStats(filePath: string): Promise<void>;
7
55
  export declare function recordAttempt(accountLabel: string, accountType: string): void;
8
56
  export declare function recordFinalSuccess(accountLabel?: string, accountType?: string): void;
9
57
  export declare function recordAttemptError(accountLabel: string, accountType: string, status: number, rateLimitKind?: "transient" | "quota"): void;
10
58
  export declare function recordFinalError(_status: number, accountLabel?: string, accountType?: string): void;
11
59
  export declare function getStats(): ProxyStats;
60
+ export declare function getReconciledStats(): Promise<ProxyStats>;
12
61
  export declare function getAccountStats(label: string): AccountStats | undefined;
62
+ export declare function getUsageStatsPersistenceStatus(): ProxyStatsPersistenceStatus;
63
+ export declare function flushUsageStats(): Promise<void>;
64
+ /** Reset process-local counters while intentionally preserving durable state. */
13
65
  export declare function resetStats(): void;
66
+ /** Disconnect persistence and clear singleton state between isolated tests. */
67
+ export declare function resetUsageStatsForTests(): Promise<void>;