@juspay/neurolink 10.4.3 → 10.4.4

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.
@@ -429,7 +429,11 @@ export function createSSEInterceptor(options = {}) {
429
429
  // Wrap the writable side so we can intercept abort() — which does NOT
430
430
  // trigger the TransformStream's flush() or cancel() callbacks.
431
431
  const innerWriter = transform.writable.getWriter();
432
+ let writableController;
432
433
  const writable = new WritableStream({
434
+ start(controller) {
435
+ writableController = controller;
436
+ },
433
437
  write(chunk) {
434
438
  return innerWriter.write(chunk);
435
439
  },
@@ -441,8 +445,50 @@ export function createSSEInterceptor(options = {}) {
441
445
  return innerWriter.abort(reason);
442
446
  },
443
447
  });
448
+ // Preserve downstream cancellation across the wrapped writable. Without
449
+ // this bridge, client disconnects leave the upstream source and telemetry
450
+ // promise open indefinitely after the readable side is cancelled.
451
+ void innerWriter.closed.catch((reason) => {
452
+ settle();
453
+ try {
454
+ writableController.error(reason);
455
+ }
456
+ catch {
457
+ // The wrapper may already be closing or aborted.
458
+ }
459
+ });
460
+ const innerReader = transform.readable.getReader();
461
+ const readable = new ReadableStream({
462
+ async pull(controller) {
463
+ try {
464
+ const { done, value } = await innerReader.read();
465
+ if (done) {
466
+ controller.close();
467
+ }
468
+ else {
469
+ controller.enqueue(value);
470
+ }
471
+ }
472
+ catch (error) {
473
+ controller.error(error);
474
+ }
475
+ },
476
+ async cancel(reason) {
477
+ settle();
478
+ try {
479
+ writableController.error(reason);
480
+ }
481
+ catch {
482
+ // The wrapper may already be closing or aborted.
483
+ }
484
+ await Promise.allSettled([
485
+ innerReader.cancel(reason),
486
+ innerWriter.abort(reason),
487
+ ]);
488
+ },
489
+ });
444
490
  const stream = {
445
- readable: transform.readable,
491
+ readable,
446
492
  writable,
447
493
  };
448
494
  return { stream, telemetry: telemetryPromise };
@@ -8,5 +8,11 @@ export declare function isPermanentRefreshFailure(result: RefreshResult): boolea
8
8
  * loaded either the old token or the newly rotated token around persistence.
9
9
  */
10
10
  export declare function refreshToken(account: RefreshableAccount): Promise<RefreshResult>;
11
+ /**
12
+ * Refreshes against the latest persisted credential generation. Queued
13
+ * requests can otherwise reject a rotated refresh token and disable a newer
14
+ * login that was saved while they were waiting.
15
+ */
16
+ export declare function refreshTokenFromLatest(account: RefreshableAccount, target?: TokenPersistTarget): Promise<RefreshResult>;
11
17
  export declare function clearRefreshStateForTests(): void;
12
18
  export declare function persistTokens(target: TokenPersistTarget, account: RefreshableAccount): Promise<void>;
@@ -1,12 +1,14 @@
1
1
  import { logger } from "../utils/logger.js";
2
2
  import { tokenStore } from "../auth/tokenStore.js";
3
+ import { ANTHROPIC_TOKEN_URL, ANTHROPIC_TOKEN_URL_FALLBACK, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_CLIENT_ID, } from "../auth/anthropicOAuth.js";
3
4
  import { writeJsonSnapshotAtomically } from "./snapshotPersistence.js";
4
- const REFRESH_URL = "https://api.anthropic.com/v1/oauth/token";
5
- const REFRESH_URL_FALLBACK = "https://console.anthropic.com/v1/oauth/token";
6
- const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
5
+ import { withTimeout } from "../utils/async/withTimeout.js";
6
+ import { redactUrlForError } from "../utils/logSanitize.js";
7
7
  const BUFFER_MS = 5 * 60 * 1000;
8
8
  const SUCCESS_CACHE_MS = 60_000;
9
- const USER_AGENT = "claude-cli/2.1.80 (external, cli)";
9
+ // Bound the best-effort persisted-credential reconciliation read so a hung or
10
+ // slow token-store decrypt can never stall (or abort) a live token refresh.
11
+ const PEEK_TOKENS_TIMEOUT_MS = 2_000;
10
12
  const refreshesInFlight = new Map();
11
13
  export function needsRefresh(account) {
12
14
  return !!(account.expiresAt &&
@@ -23,28 +25,33 @@ async function performTokenRefresh(account) {
23
25
  if (!account.refreshToken) {
24
26
  return { success: false, error: "No refresh token available" };
25
27
  }
26
- const formBody = new URLSearchParams({
28
+ // OAuth 2.0 token endpoints require an `application/x-www-form-urlencoded`
29
+ // request body (RFC 6749 §6); a JSON body is nonstandard for this grant and
30
+ // is rejected by RFC-compliant endpoints. Mirror the interactive auth flow
31
+ // in `anthropicOAuth.ts::_refreshAccessToken`.
32
+ const requestBody = new URLSearchParams({
27
33
  grant_type: "refresh_token",
28
34
  refresh_token: account.refreshToken,
29
- client_id: CLIENT_ID,
35
+ client_id: CLAUDE_CODE_CLIENT_ID,
30
36
  }).toString();
31
37
  const headers = {
32
38
  "Content-Type": "application/x-www-form-urlencoded",
33
- "User-Agent": USER_AGENT,
39
+ Accept: "application/json",
40
+ "User-Agent": CLAUDE_CLI_USER_AGENT,
34
41
  };
35
- const urls = [REFRESH_URL, REFRESH_URL_FALLBACK];
42
+ const urls = [ANTHROPIC_TOKEN_URL, ANTHROPIC_TOKEN_URL_FALLBACK];
36
43
  let terminalFailure;
37
44
  for (const url of urls) {
38
45
  try {
39
46
  const resp = await fetch(url, {
40
47
  method: "POST",
41
48
  headers,
42
- body: formBody,
49
+ body: requestBody,
43
50
  signal: AbortSignal.timeout(10_000),
44
51
  });
45
52
  if (!resp.ok) {
46
53
  const errorBody = await resp.text();
47
- logger.warn(`[token-refresh] failed for ${account.label} at ${url}`, {
54
+ logger.warn(`[token-refresh] failed for ${account.label} at ${redactUrlForError(url)}`, {
48
55
  status: resp.status,
49
56
  error: errorBody.slice(0, 500),
50
57
  });
@@ -57,7 +64,7 @@ async function performTokenRefresh(account) {
57
64
  terminalFailure = failure;
58
65
  }
59
66
  // If primary URL returned a non-ok status, try fallback.
60
- if (url === REFRESH_URL) {
67
+ if (url === ANTHROPIC_TOKEN_URL) {
61
68
  continue;
62
69
  }
63
70
  return terminalFailure ?? failure;
@@ -75,11 +82,11 @@ async function performTokenRefresh(account) {
75
82
  return { success: true };
76
83
  }
77
84
  catch (e) {
78
- logger.warn(`[token-refresh] exception for ${account.label} at ${url}`, {
85
+ logger.warn(`[token-refresh] exception for ${account.label} at ${redactUrlForError(url)}`, {
79
86
  error: String(e),
80
87
  });
81
88
  // If primary URL threw, try fallback
82
- if (url === REFRESH_URL) {
89
+ if (url === ANTHROPIC_TOKEN_URL) {
83
90
  continue;
84
91
  }
85
92
  return terminalFailure ?? { success: false, error: String(e) };
@@ -151,6 +158,54 @@ export async function refreshToken(account) {
151
158
  }
152
159
  return shared.result;
153
160
  }
161
+ async function reloadPersistedTokenStoreAccount(account, target) {
162
+ if (!target || typeof target === "string" || !("providerKey" in target)) {
163
+ return false;
164
+ }
165
+ // Best-effort, bounded reconciliation: this only adopts a newer persisted
166
+ // credential generation if one exists. A decryption/IO failure — or a hung
167
+ // read — must NOT abort the caller, which can still refresh with the
168
+ // already-loaded token. Swallow the failure and treat it as "no newer
169
+ // persisted generation found" so refresh proceeds instead of the account
170
+ // being spuriously disabled.
171
+ let stored;
172
+ try {
173
+ stored = await withTimeout(tokenStore.peekTokens(target.providerKey), PEEK_TOKENS_TIMEOUT_MS, "[token-refresh] peekTokens reconciliation timed out");
174
+ }
175
+ catch (err) {
176
+ logger.debug("[token-refresh] skipping persisted-state reconciliation (peek failed)", { error: err instanceof Error ? err.message : String(err) });
177
+ return false;
178
+ }
179
+ if (!stored ||
180
+ stored.expiresAt <= (account.expiresAt ?? 0) ||
181
+ (stored.accessToken === account.token &&
182
+ stored.refreshToken === account.refreshToken &&
183
+ stored.expiresAt === account.expiresAt)) {
184
+ return false;
185
+ }
186
+ account.token = stored.accessToken;
187
+ account.refreshToken = stored.refreshToken;
188
+ account.expiresAt = stored.expiresAt;
189
+ return true;
190
+ }
191
+ /**
192
+ * Refreshes against the latest persisted credential generation. Queued
193
+ * requests can otherwise reject a rotated refresh token and disable a newer
194
+ * login that was saved while they were waiting.
195
+ */
196
+ export async function refreshTokenFromLatest(account, target) {
197
+ const reloadedBeforeRefresh = await reloadPersistedTokenStoreAccount(account, target);
198
+ if (reloadedBeforeRefresh && !needsRefresh(account)) {
199
+ return { success: true };
200
+ }
201
+ const result = await refreshToken(account);
202
+ if (result.success) {
203
+ return result;
204
+ }
205
+ return (await reloadPersistedTokenStoreAccount(account, target))
206
+ ? { success: true }
207
+ : result;
208
+ }
154
209
  export function clearRefreshStateForTests() {
155
210
  refreshesInFlight.clear();
156
211
  }
@@ -183,7 +238,7 @@ async function persistLegacyCredentials(credPath, account) {
183
238
  }
184
239
  async function persistTokenStoreAccount(providerKey, account) {
185
240
  try {
186
- const existing = await tokenStore.loadTokens(providerKey);
241
+ const existing = await withTimeout(tokenStore.peekTokens(providerKey), PEEK_TOKENS_TIMEOUT_MS, "[token-refresh] peekTokens for persistence timed out");
187
242
  const merged = {
188
243
  accessToken: account.token,
189
244
  refreshToken: account.refreshToken ?? existing?.refreshToken,
@@ -191,7 +246,7 @@ async function persistTokenStoreAccount(providerKey, account) {
191
246
  tokenType: existing?.tokenType ?? "Bearer",
192
247
  ...(existing?.scope ? { scope: existing.scope } : {}),
193
248
  };
194
- await tokenStore.saveTokens(providerKey, merged);
249
+ await withTimeout(tokenStore.saveTokens(providerKey, merged), PEEK_TOKENS_TIMEOUT_MS, "[token-refresh] saveTokens timed out");
195
250
  }
196
251
  catch (err) {
197
252
  logger.warn("[token-refresh] Failed to persist TokenStore credentials", {
@@ -5,34 +5,49 @@
5
5
  * shared snapshot asynchronously, under a cross-process lock, so overlapping
6
6
  * rolling workers cannot overwrite each other's counters.
7
7
  */
8
- import type { AccountStats, ProxyStats, ProxyStatsPersistenceStatus, ProxyUsageStatsStoreOptions } from "../types/index.js";
8
+ import type { AccountStats, ProxyTerminalErrorDetails, ProxyTerminalErrorJournal, ProxyStats, ProxyStatsPersistenceStatus, ProxyUsageStatsSnapshot, ProxyUsageStatsStoreOptions } from "../types/index.js";
9
9
  export declare class ProxyUsageStatsStore {
10
10
  private readonly now;
11
11
  private readonly flushIntervalMs;
12
12
  private readonly lockTimeoutMs;
13
13
  private readonly staleLockMs;
14
14
  private filePath?;
15
+ private terminalErrorsFilePath?;
15
16
  private stats;
16
17
  private pending;
18
+ private terminalErrors;
19
+ private pendingTerminalErrors;
17
20
  private pendingMutations;
18
21
  private inFlightMutations;
22
+ private pendingTerminalErrorMutations;
23
+ private inFlightTerminalErrorMutations;
19
24
  private revision;
25
+ private terminalErrorsRevision;
20
26
  private flushTimer;
21
27
  private readonly flushMutex;
22
28
  private lastFlushedAt?;
23
29
  private lastReconciledAt?;
24
30
  private lastRecoveryAt?;
25
31
  private lastError?;
32
+ private terminalErrorsLastFlushedAt?;
33
+ private terminalErrorsLastRecoveryAt?;
34
+ private terminalErrorsLastError?;
35
+ private snapshotVersion;
26
36
  constructor(options?: ProxyUsageStatsStoreOptions);
27
37
  initialize(filePath?: string): Promise<void>;
28
38
  recordAttempt(accountLabel: string, accountType: string): void;
29
39
  recordFinalSuccess(accountLabel?: string, accountType?: string): void;
30
40
  recordAttemptError(accountLabel: string, accountType: string, status: number, rateLimitKind?: "transient" | "quota"): void;
31
- recordFinalError(_status: number, accountLabel?: string, accountType?: string): void;
41
+ recordFinalError(status: number, accountLabel?: string, accountType?: string, details?: ProxyTerminalErrorDetails): void;
32
42
  getStats(): ProxyStats;
33
43
  getAccountStats(label: string): AccountStats | undefined;
44
+ getTerminalErrors(): ProxyTerminalErrorJournal;
45
+ getUsageSnapshot(): ProxyUsageStatsSnapshot;
34
46
  getPersistenceStatus(): ProxyStatsPersistenceStatus;
35
47
  flush(): Promise<void>;
48
+ private persistStatsDelta;
49
+ private persistTerminalErrorDelta;
50
+ reconcileUsageSnapshot(): Promise<ProxyUsageStatsSnapshot>;
36
51
  reconcile(): Promise<ProxyStats>;
37
52
  resetMemory(): void;
38
53
  resetForTests(): Promise<void>;
@@ -41,13 +56,18 @@ export declare class ProxyUsageStatsStore {
41
56
  private applyFinalSuccess;
42
57
  private applyAttemptError;
43
58
  private applyFinalError;
59
+ private applyTerminalError;
44
60
  private ensureAccount;
45
61
  private scheduleFlush;
46
62
  private cancelFlushTimer;
47
63
  private readSnapshot;
64
+ private readTerminalErrorSnapshot;
48
65
  private applySnapshot;
66
+ private applyTerminalErrorSnapshot;
49
67
  private recoverCorruptSnapshot;
68
+ private recoverCorruptTerminalErrorSnapshot;
50
69
  private quarantineCorruptSnapshot;
70
+ private quarantineCorruptTerminalErrorSnapshot;
51
71
  private pruneCorruptSnapshots;
52
72
  private describeError;
53
73
  }
@@ -55,10 +75,12 @@ export declare function initUsageStats(filePath: string): Promise<void>;
55
75
  export declare function recordAttempt(accountLabel: string, accountType: string): void;
56
76
  export declare function recordFinalSuccess(accountLabel?: string, accountType?: string): void;
57
77
  export declare function recordAttemptError(accountLabel: string, accountType: string, status: number, rateLimitKind?: "transient" | "quota"): void;
58
- export declare function recordFinalError(_status: number, accountLabel?: string, accountType?: string): void;
78
+ export declare function recordFinalError(status: number, accountLabel?: string, accountType?: string, details?: ProxyTerminalErrorDetails): void;
59
79
  export declare function getStats(): ProxyStats;
60
80
  export declare function getReconciledStats(): Promise<ProxyStats>;
81
+ export declare function getReconciledUsageSnapshot(): Promise<ProxyUsageStatsSnapshot>;
61
82
  export declare function getAccountStats(label: string): AccountStats | undefined;
83
+ export declare function getTerminalErrors(): ProxyTerminalErrorJournal;
62
84
  export declare function getUsageStatsPersistenceStatus(): ProxyStatsPersistenceStatus;
63
85
  export declare function flushUsageStats(): Promise<void>;
64
86
  /** Reset process-local counters while intentionally preserving durable state. */