@juspay/neurolink 10.4.2 → 10.4.3

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/neurolink.min.js +333 -336
  3. package/dist/cli/commands/proxy.d.ts +1 -1
  4. package/dist/cli/commands/proxy.js +124 -56
  5. package/dist/cli/commands/proxyAnalyze.js +10 -1
  6. package/dist/lib/proxy/proxyAnalysis.js +136 -12
  7. package/dist/lib/proxy/requestLogger.d.ts +2 -0
  8. package/dist/lib/proxy/requestLogger.js +72 -13
  9. package/dist/lib/proxy/rollingProxyServer.js +79 -8
  10. package/dist/lib/proxy/rollingWorkerProcess.js +20 -15
  11. package/dist/lib/proxy/rollingWorkerProtocol.js +3 -0
  12. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
  13. package/dist/lib/proxy/rollingWorkerSupervisor.js +31 -11
  14. package/dist/lib/proxy/runtimeConfig.d.ts +1 -0
  15. package/dist/lib/proxy/runtimeConfig.js +20 -5
  16. package/dist/lib/proxy/socketWorkerRuntime.js +41 -13
  17. package/dist/lib/server/routes/claudeProxyRoutes.js +63 -75
  18. package/dist/lib/types/proxy.d.ts +31 -1
  19. package/dist/proxy/proxyAnalysis.js +136 -12
  20. package/dist/proxy/requestLogger.d.ts +2 -0
  21. package/dist/proxy/requestLogger.js +72 -13
  22. package/dist/proxy/rollingProxyServer.js +79 -8
  23. package/dist/proxy/rollingWorkerProcess.js +20 -15
  24. package/dist/proxy/rollingWorkerProtocol.js +3 -0
  25. package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
  26. package/dist/proxy/rollingWorkerSupervisor.js +31 -11
  27. package/dist/proxy/runtimeConfig.d.ts +1 -0
  28. package/dist/proxy/runtimeConfig.js +20 -5
  29. package/dist/proxy/socketWorkerRuntime.js +41 -13
  30. package/dist/server/routes/claudeProxyRoutes.js +63 -75
  31. package/dist/types/proxy.d.ts +31 -1
  32. package/package.json +1 -1
@@ -8,6 +8,7 @@ function isTransferableProxySocket(handle) {
8
8
  typeof candidate.resume === "function" &&
9
9
  typeof candidate.destroy === "function" &&
10
10
  typeof candidate.end === "function" &&
11
+ typeof candidate.off === "function" &&
11
12
  typeof candidate.once === "function");
12
13
  }
13
14
  function destroyTransferredHandle(handle) {
@@ -153,6 +154,16 @@ export function attachSocketWorkerProcess(server, input) {
153
154
  // synchronously here would truncate that in-flight request.
154
155
  setImmediate(() => runtime.drain());
155
156
  };
157
+ const takePendingSocket = (socketId) => {
158
+ const pending = pendingSockets.get(socketId);
159
+ if (!pending) {
160
+ return undefined;
161
+ }
162
+ pendingSockets.delete(socketId);
163
+ pending.socket.off("error", pending.onError);
164
+ pending.socket.off("close", pending.onClose);
165
+ return pending.socket;
166
+ };
156
167
  const onMessage = (message, handle) => {
157
168
  if (message &&
158
169
  typeof message === "object" &&
@@ -176,7 +187,27 @@ export function attachSocketWorkerProcess(server, input) {
176
187
  socket.destroy();
177
188
  return;
178
189
  }
179
- pendingSockets.set(socketId, socket);
190
+ // A client can reset while the transferred handle is paused between
191
+ // acceptance and commit. The HTTP server has not seen the socket yet,
192
+ // so it cannot install its normal transport-error handler for us.
193
+ const onError = () => {
194
+ if (pendingSockets.get(socketId)?.socket !== socket) {
195
+ return;
196
+ }
197
+ takePendingSocket(socketId);
198
+ socket.destroy();
199
+ drainWhenPendingSettled();
200
+ };
201
+ const onClose = () => {
202
+ if (pendingSockets.get(socketId)?.socket !== socket) {
203
+ return;
204
+ }
205
+ takePendingSocket(socketId);
206
+ drainWhenPendingSettled();
207
+ };
208
+ pendingSockets.set(socketId, { socket, onError, onClose });
209
+ socket.once("error", onError);
210
+ socket.once("close", onClose);
180
211
  try {
181
212
  process.send({
182
213
  type: "proxy-worker:socket-accepted",
@@ -185,14 +216,14 @@ export function attachSocketWorkerProcess(server, input) {
185
216
  socketId,
186
217
  }, (error) => {
187
218
  if (error) {
188
- pendingSockets.delete(socketId);
189
- socket.destroy();
219
+ takePendingSocket(socketId)?.destroy();
220
+ drainWhenPendingSettled();
190
221
  }
191
222
  });
192
223
  }
193
224
  catch {
194
- pendingSockets.delete(socketId);
195
- socket.destroy();
225
+ takePendingSocket(socketId)?.destroy();
226
+ drainWhenPendingSettled();
196
227
  }
197
228
  }
198
229
  else {
@@ -229,11 +260,10 @@ export function attachSocketWorkerProcess(server, input) {
229
260
  }
230
261
  else if (message.type === "proxy-worker:socket-commit" ||
231
262
  message.type === "proxy-worker:socket-cancel") {
232
- const socket = pendingSockets.get(message.socketId);
263
+ const socket = takePendingSocket(message.socketId);
233
264
  if (!socket) {
234
265
  return;
235
266
  }
236
- pendingSockets.delete(message.socketId);
237
267
  if (message.type === "proxy-worker:socket-commit") {
238
268
  runtime.acceptSocket(socket);
239
269
  }
@@ -259,10 +289,9 @@ export function attachSocketWorkerProcess(server, input) {
259
289
  // is exiting. The zero-downtime guarantee applies to the rolling handoff
260
290
  // (control-message) path, not to process termination.
261
291
  const drain = () => {
262
- for (const socket of pendingSockets.values()) {
263
- socket.destroy();
292
+ for (const socketId of [...pendingSockets.keys()]) {
293
+ takePendingSocket(socketId)?.destroy();
264
294
  }
265
- pendingSockets.clear();
266
295
  runtime.drain();
267
296
  };
268
297
  const onTerminationSignal = () => drain();
@@ -283,10 +312,9 @@ export function attachSocketWorkerProcess(server, input) {
283
312
  process.off("disconnect", drain);
284
313
  process.off("SIGTERM", onTerminationSignal);
285
314
  process.off("SIGINT", onTerminationSignal);
286
- for (const socket of pendingSockets.values()) {
287
- socket.destroy();
315
+ for (const socketId of [...pendingSockets.keys()]) {
316
+ takePendingSocket(socketId)?.destroy();
288
317
  }
289
- pendingSockets.clear();
290
318
  runtime.close();
291
319
  },
292
320
  };
@@ -2241,6 +2241,7 @@ async function handleAnthropicSuccessfulResponse(args) {
2241
2241
  attemptNumber,
2242
2242
  finalBodyStr,
2243
2243
  upstreamSpan,
2244
+ logAttempt,
2244
2245
  logProxyBody,
2245
2246
  logFinalRequest,
2246
2247
  });
@@ -2248,6 +2249,8 @@ async function handleAnthropicSuccessfulResponse(args) {
2248
2249
  async function handleAnthropicStreamingSuccessResponse(args) {
2249
2250
  const { account, accountState, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
2250
2251
  if (!response.body) {
2252
+ recordAttemptError(account.label, account.type, 502);
2253
+ logAttempt(502, "stream_error", "No response body from upstream");
2251
2254
  upstreamSpan?.end();
2252
2255
  tracer?.setError("stream_error", "No response body from upstream");
2253
2256
  tracer?.end(502, Date.now() - requestStartTime);
@@ -2386,6 +2389,9 @@ async function handleAnthropicStreamingSuccessResponse(args) {
2386
2389
  failure: { message: preflight.message, rateLimit: isRateLimit },
2387
2390
  };
2388
2391
  }
2392
+ logAttempt(response.status, undefined, undefined, {
2393
+ attemptDurationMs: Date.now() - fetchStartMs,
2394
+ });
2389
2395
  const streamOutcomeTracker = createStreamTerminalOutcomeTracker();
2390
2396
  let mainStreamClosed = false;
2391
2397
  const remainingStream = new ReadableStream({
@@ -2693,8 +2699,11 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2693
2699
  });
2694
2700
  }
2695
2701
  async function handleAnthropicJsonSuccessResponse(args) {
2696
- const { account, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
2702
+ const { account, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
2697
2703
  const responseText = await response.text();
2704
+ logAttempt(response.status, undefined, undefined, {
2705
+ attemptDurationMs: Date.now() - fetchStartMs,
2706
+ });
2698
2707
  tracer?.logUpstreamResponseBody(responseText);
2699
2708
  logProxyBody({
2700
2709
  phase: "upstream_response",
@@ -2779,8 +2788,8 @@ async function handleAnthropicJsonSuccessResponse(args) {
2779
2788
  }
2780
2789
  return { response: responseJson };
2781
2790
  }
2782
- async function handleAnthropicSuccessfulRetryResponse(args) {
2783
- const { body, account, accountState, retryResp, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
2791
+ async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
2792
+ const { account, accountState, retryResp, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
2784
2793
  const retryQuota = parseQuotaHeaders(retryResp.headers);
2785
2794
  if (retryQuota) {
2786
2795
  // Keep the auth-retry success path in parity with the main success path:
@@ -2802,63 +2811,11 @@ async function handleAnthropicSuccessfulRetryResponse(args) {
2802
2811
  });
2803
2812
  });
2804
2813
  }
2805
- if (body.stream && retryResp.body) {
2806
- const retryReader = retryResp.body.getReader();
2807
- const streamOutcomeTracker = createStreamTerminalOutcomeTracker();
2808
- let retryStreamClosed = false;
2809
- const retryStream = new ReadableStream({
2810
- async pull(controller) {
2811
- if (retryStreamClosed) {
2812
- return;
2813
- }
2814
- try {
2815
- const { done, value } = await retryReader.read();
2816
- if (retryStreamClosed) {
2817
- return;
2818
- }
2819
- if (done) {
2820
- retryStreamClosed = true;
2821
- streamOutcomeTracker.complete();
2822
- controller.close();
2823
- return;
2824
- }
2825
- controller.enqueue(value);
2826
- }
2827
- catch (streamErr) {
2828
- const errMsg = describeTransportError(streamErr);
2829
- logger.always(`[proxy] mid-stream error (auth-retry) account=${account.label}: ${errMsg}`);
2830
- streamOutcomeTracker.fail(errMsg);
2831
- if (!retryStreamClosed) {
2832
- retryStreamClosed = true;
2833
- const errorEvent = `event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: `Upstream stream interrupted: ${errMsg}` } })}\n\n`;
2834
- controller.enqueue(new TextEncoder().encode(errorEvent));
2835
- controller.close();
2836
- }
2837
- }
2838
- },
2839
- cancel() {
2840
- retryStreamClosed = true;
2841
- streamOutcomeTracker.cancel();
2842
- return retryReader.cancel();
2843
- },
2844
- });
2845
- return attachAnthropicSuccessStreamTelemetry({
2846
- account,
2847
- response: retryResp,
2848
- responseHeaders: Object.fromEntries([...retryResp.headers.entries()]),
2849
- remainingStream: retryStream,
2850
- streamOutcome: streamOutcomeTracker.outcome,
2851
- tracer,
2852
- requestStartTime,
2853
- attemptNumber,
2854
- finalBodyStr,
2855
- upstreamSpan,
2856
- logProxyBody,
2857
- logFinalRequest,
2858
- });
2859
- }
2860
2814
  const retryRespHeaders = Object.fromEntries([...retryResp.headers.entries()]);
2861
2815
  const retryText = await retryResp.text();
2816
+ logAttempt(retryResp.status, undefined, undefined, {
2817
+ attemptDurationMs: Date.now() - fetchStartMs,
2818
+ });
2862
2819
  tracer?.logUpstreamResponseHeaders(retryRespHeaders);
2863
2820
  tracer?.logUpstreamResponseBody(retryText);
2864
2821
  logProxyBody({
@@ -2960,7 +2917,7 @@ async function handleAnthropicAuthRetry(args) {
2960
2917
  const retryLogAttempt = (status, errorType, errorMessage, extra) => logAttempt(status, errorType, errorMessage, {
2961
2918
  ...extra,
2962
2919
  attempt: retryAttemptNumber,
2963
- attemptDurationMs: Date.now() - retryAttemptStartedAt,
2920
+ attemptDurationMs: extra?.attemptDurationMs ?? Date.now() - retryAttemptStartedAt,
2964
2921
  });
2965
2922
  const retryBodyStr = buildUpstreamBody(account.token).bodyStr;
2966
2923
  const retryFetchStartMs = Date.now();
@@ -2986,23 +2943,54 @@ async function handleAnthropicAuthRetry(args) {
2986
2943
  authRetrySucceeded = true;
2987
2944
  accountState.consecutiveRefreshFailures = 0;
2988
2945
  logger.always(`[proxy] ← 200 account=${account.label} (after ${authRetry + 1} refresh(es))`);
2989
- const successResponse = await handleAnthropicSuccessfulRetryResponse({
2990
- ctx,
2991
- body,
2992
- account,
2993
- accountState,
2994
- retryResp,
2995
- tracer,
2996
- requestStartTime,
2997
- fetchStartMs: retryFetchStartMs,
2998
- attemptNumber: retryAttemptNumber,
2999
- finalBodyStr: retryBodyStr,
3000
- upstreamSpan: currentUpstreamSpan,
3001
- logProxyBody,
3002
- logFinalRequest,
3003
- });
2946
+ const successResult = body.stream
2947
+ ? await handleAnthropicSuccessfulResponse({
2948
+ ctx,
2949
+ body,
2950
+ account,
2951
+ accountState,
2952
+ response: retryResp,
2953
+ tracer,
2954
+ requestStartTime,
2955
+ fetchStartMs: retryFetchStartMs,
2956
+ attemptNumber: retryAttemptNumber,
2957
+ finalBodyStr: retryBodyStr,
2958
+ upstreamSpan: currentUpstreamSpan,
2959
+ logAttempt: retryLogAttempt,
2960
+ logProxyBody,
2961
+ logFinalRequest,
2962
+ })
2963
+ : {
2964
+ response: await handleAnthropicSuccessfulNonStreamRetryResponse({
2965
+ account,
2966
+ accountState,
2967
+ retryResp,
2968
+ tracer,
2969
+ requestStartTime,
2970
+ fetchStartMs: retryFetchStartMs,
2971
+ attemptNumber: retryAttemptNumber,
2972
+ finalBodyStr: retryBodyStr,
2973
+ upstreamSpan: currentUpstreamSpan,
2974
+ logAttempt: retryLogAttempt,
2975
+ logProxyBody,
2976
+ logFinalRequest,
2977
+ }),
2978
+ };
2979
+ if ("retryNextAccount" in successResult) {
2980
+ const failure = successResult.failure;
2981
+ return {
2982
+ continueLoop: true,
2983
+ lastError: failure?.message ?? currentLastError,
2984
+ authFailureMessage: currentAuthFailureMessage,
2985
+ sawRateLimit: currentSawRateLimit || Boolean(failure?.rateLimit),
2986
+ sawTransientFailure: currentSawTransientFailure ||
2987
+ Boolean(failure && !failure.rateLimit),
2988
+ sawNetworkError: currentSawNetworkError,
2989
+ upstreamSpan: undefined,
2990
+ };
2991
+ }
3004
2992
  return {
3005
- response: successResponse,
2993
+ response: successResult.response,
3006
2994
  continueLoop: false,
3007
2995
  lastError: currentLastError,
3008
2996
  authFailureMessage: currentAuthFailureMessage,
@@ -1121,6 +1121,7 @@ export type ProxyAnalysisAccount = {
1121
1121
  unclassifiedRateLimits: number;
1122
1122
  };
1123
1123
  /** Offline report generated from proxy request, attempt, and lifecycle logs. */
1124
+ export type ProxyAnalysisStreamName = "lifecycle" | "requests" | "attempts" | "debug";
1124
1125
  export type ProxyAnalysisReport = {
1125
1126
  generatedAt: string;
1126
1127
  since: string;
@@ -1129,6 +1130,7 @@ export type ProxyAnalysisReport = {
1129
1130
  lifecycle: number;
1130
1131
  requests: number;
1131
1132
  attempts: number;
1133
+ debug: number;
1132
1134
  };
1133
1135
  coverage: {
1134
1136
  lifecycle: boolean;
@@ -1143,6 +1145,20 @@ export type ProxyAnalysisReport = {
1143
1145
  unsupportedLifecycleLines: number;
1144
1146
  lifecycleSequenceGaps: number;
1145
1147
  lifecycleSequenceDuplicates: number;
1148
+ streams: Record<ProxyAnalysisStreamName, {
1149
+ observedFrom: string | null;
1150
+ observedTo: string | null;
1151
+ startsAtOrBeforeRequestedWindow: boolean;
1152
+ }>;
1153
+ bodyArtifacts: {
1154
+ capturesIndexed: number;
1155
+ artifactsReferenced: number;
1156
+ artifactsPresent: number;
1157
+ artifactsMissing: number;
1158
+ invalidPaths: number;
1159
+ writeFailures: number;
1160
+ truncatedCaptures: number;
1161
+ };
1146
1162
  };
1147
1163
  lifecycle: {
1148
1164
  accepted: number;
@@ -1392,6 +1408,7 @@ export type StoredBodyArtifact = {
1392
1408
  storedFileBytes?: number;
1393
1409
  redactedBody?: string;
1394
1410
  bodyTruncated?: boolean;
1411
+ bodyWriteFailed?: boolean;
1395
1412
  };
1396
1413
  /** File the proxy logger tracks for rotation and cleanup. */
1397
1414
  export type ManagedLogFile = {
@@ -1653,6 +1670,11 @@ export type ProxyWorkerStatusMessage = {
1653
1670
  generation: number;
1654
1671
  pid: number;
1655
1672
  socketId: string;
1673
+ } | {
1674
+ type: "proxy-worker:replacement-requested";
1675
+ generation: number;
1676
+ pid: number;
1677
+ reason: "environment";
1656
1678
  };
1657
1679
  export type ProxyWorkerSocketMessage = {
1658
1680
  type: "proxy-worker:socket";
@@ -1661,6 +1683,8 @@ export type ProxyWorkerSocketMessage = {
1661
1683
  };
1662
1684
  export type ProxyWorkerIpcMessage = ProxyWorkerControlMessage | ProxyWorkerStatusMessage | ProxyWorkerSocketMessage;
1663
1685
  export type TransferableProxySocket = Pick<import("node:net").Socket, "destroy" | "end" | "pause" | "resume" | "once">;
1686
+ /** A transferred socket whose temporary handoff listeners can be detached. */
1687
+ export type DetachableTransferableProxySocket = TransferableProxySocket & Pick<import("node:net").Socket, "off">;
1664
1688
  export type RollingWorkerHandle = {
1665
1689
  pid: number;
1666
1690
  sendControl: (message: ProxyWorkerControlMessage) => void;
@@ -1714,6 +1738,11 @@ export type RollingWorkerSupervisorOptions = {
1714
1738
  socketQueueTimeoutMs?: number;
1715
1739
  shutdownTimeoutMs?: number;
1716
1740
  onStateChange?: (snapshot: RollingWorkerSupervisorSnapshot) => void;
1741
+ onReplacementRequested?: (request: {
1742
+ generation: number;
1743
+ pid: number;
1744
+ reason: "environment";
1745
+ }) => void;
1717
1746
  log?: (message: string) => void;
1718
1747
  };
1719
1748
  export type RollingProxyServerOptions = {
@@ -1854,6 +1883,7 @@ export type ProxyRuntimeConfigReloadResult = {
1854
1883
  applied: boolean;
1855
1884
  changed: boolean;
1856
1885
  generation: number;
1886
+ environmentChanged?: boolean;
1857
1887
  error?: string;
1858
1888
  };
1859
1889
  /** Safe runtime configuration diagnostics exposed through proxy status. */
@@ -1933,7 +1963,7 @@ export type StatusStats = {
1933
1963
  transientRateLimits?: number;
1934
1964
  quotaRateLimits?: number;
1935
1965
  cooling: boolean;
1936
- status?: "active" | "cooling" | "disabled" | "excluded" | "removed";
1966
+ status?: "active" | "cooling" | "disabled" | "excluded" | "removed" | "unattributed";
1937
1967
  }[];
1938
1968
  persistence?: ProxyStatsPersistenceStatus;
1939
1969
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "10.4.2",
3
+ "version": "10.4.3",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {