@juspay/neurolink 9.93.1 → 9.93.2

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.
@@ -16,6 +16,7 @@ import chalk from "chalk";
16
16
  import ora from "ora";
17
17
  import { buildProxyHealthResponse, createProxyReadinessState, markProxyDrainingForUpdate, markProxyReady, resumeProxyConnections, waitForProxyReadiness, } from "../../lib/proxy/proxyHealth.js";
18
18
  import { logger } from "../../lib/utils/logger.js";
19
+ import { sanitizeForLog } from "../../lib/utils/logSanitize.js";
19
20
  import { withTimeout } from "../../lib/utils/async/withTimeout.js";
20
21
  import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
21
22
  import { configureProxyKeepAliveDispatcher } from "../../lib/proxy/proxyDispatcher.js";
@@ -25,6 +26,7 @@ import { beginProxyRequest, getProxyActivitySnapshot, trackProxyResponse, } from
25
26
  import { flushProxyLifecycleEvents, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../lib/proxy/proxyLifecycle.js";
26
27
  import { describeInstallFailure, getGlobalInstallArgs, resolveGlobalInstaller, validateInstalledVersion, } from "../../lib/proxy/globalInstaller.js";
27
28
  import { startUpdaterWorkerSupervisor } from "../../lib/proxy/updaterSupervisor.js";
29
+ import { openProxyWorkerLog } from "../../lib/proxy/workerLog.js";
28
30
  import { waitForProxyUpdateWindow } from "../../lib/proxy/updateCoordinator.js";
29
31
  import { abandonPendingUpdate, clearUpdateDeferral, isVersionSuppressed, loadUpdateState, recordCheck, recordSuccessfulUpdate, recordUpdateDeferred, recordUpdateFailure, recordUpdateInstalled, suppressVersion, } from "../../lib/proxy/updateState.js";
30
32
  import { loadProxyEnvFile, resolveProxyEnvFile, } from "../../lib/proxy/proxyEnv.js";
@@ -538,18 +540,14 @@ function spawnFailOpenGuard(host, port, parentPid) {
538
540
  String(parentPid),
539
541
  "--quiet",
540
542
  ];
541
- // Write guard stdout/stderr to a log file instead of discarding them.
542
- const { openSync, closeSync, mkdirSync, existsSync } = _require("fs");
543
- const guardLogDir = join(homedir(), ".neurolink", "logs");
544
- if (!existsSync(guardLogDir)) {
545
- mkdirSync(guardLogDir, { recursive: true });
546
- }
547
- const guardLogPath = join(guardLogDir, "proxy-guard.log");
548
- const logFd = openSync(guardLogPath, "a");
543
+ const workerLog = openProxyWorkerLog("proxy-guard.log");
544
+ if (workerLog.error) {
545
+ logger.debug(`[proxy] guard logging disabled: ${workerLog.error}`);
546
+ }
549
547
  try {
550
548
  const child = spawn(process.execPath, args, {
551
549
  detached: true,
552
- stdio: ["ignore", logFd, logFd],
550
+ stdio: ["ignore", workerLog.stdio, workerLog.stdio],
553
551
  });
554
552
  child.unref();
555
553
  return child.pid;
@@ -559,7 +557,7 @@ function spawnFailOpenGuard(host, port, parentPid) {
559
557
  return undefined;
560
558
  }
561
559
  finally {
562
- closeSync(logFd); // parent closes its copy; child keeps the fd
560
+ workerLog.close();
563
561
  }
564
562
  }
565
563
  function spawnProxyUpdater(host, port, parentPid) {
@@ -585,16 +583,14 @@ function spawnProxyUpdater(host, port, parentPid) {
585
583
  "--updater-only",
586
584
  "--quiet",
587
585
  ];
588
- const { openSync, closeSync, mkdirSync, existsSync } = _require("fs");
589
- const logsDir = join(homedir(), ".neurolink", "logs");
590
- if (!existsSync(logsDir)) {
591
- mkdirSync(logsDir, { recursive: true });
586
+ const workerLog = openProxyWorkerLog("proxy-updater.log");
587
+ if (workerLog.error) {
588
+ logger.always(`[proxy] updater logging disabled: ${workerLog.error}`);
592
589
  }
593
- const logFd = openSync(join(logsDir, "proxy-updater.log"), "a");
594
590
  try {
595
591
  const child = spawn(process.execPath, args, {
596
592
  detached: true,
597
- stdio: ["ignore", logFd, logFd],
593
+ stdio: ["ignore", workerLog.stdio, workerLog.stdio],
598
594
  env: {
599
595
  ...process.env,
600
596
  NEUROLINK_PROXY_UPDATE_CONTROL_TOKEN: PROXY_UPDATE_CONTROL_TOKEN,
@@ -614,7 +610,7 @@ function spawnProxyUpdater(host, port, parentPid) {
614
610
  return undefined;
615
611
  }
616
612
  finally {
617
- closeSync(logFd);
613
+ workerLog.close();
618
614
  }
619
615
  }
620
616
  async function runProxyTelemetryManager(command) {
@@ -1065,7 +1061,8 @@ export async function createProxyStartApp(params) {
1065
1061
  metadata.stream = stream === "stream";
1066
1062
  metadata.toolCount = toolCount;
1067
1063
  }
1068
- logger.always(`[proxy] ${c.req.method} ${c.req.path} model=${model} ${stream} tools=${toolCount}`);
1064
+ const logModel = sanitizeForLog(String(model));
1065
+ logger.always(`[proxy] ${c.req.method} ${c.req.path} → model=${logModel} ${stream} tools=${toolCount}`);
1069
1066
  const ctx = {
1070
1067
  requestId: metadata?.requestId ?? crypto.randomUUID(),
1071
1068
  method: c.req.method,
@@ -112,7 +112,10 @@ export class StateFileManager {
112
112
  if (!fs.existsSync(dir)) {
113
113
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
114
114
  }
115
- fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2));
115
+ fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), {
116
+ mode: 0o600,
117
+ });
118
+ fs.chmodSync(this.filePath, 0o600);
116
119
  }
117
120
  /**
118
121
  * Load state from the state file
@@ -14,7 +14,7 @@ function writableDirectory(path) {
14
14
  if (!existsSync(path)) {
15
15
  return false;
16
16
  }
17
- accessSync(path, constants.W_OK);
17
+ accessSync(path, constants.W_OK | constants.X_OK);
18
18
  return true;
19
19
  }
20
20
  catch {
@@ -760,6 +760,7 @@ export function createClaudeToOpenAIStreamTransform(requestModel, options = {})
760
760
  finished = true;
761
761
  options.onError?.(message);
762
762
  emit(controller, serializer.emitError(message));
763
+ controller.terminate();
763
764
  return;
764
765
  }
765
766
  default:
@@ -337,7 +337,7 @@ function processEvent(acc, event) {
337
337
  const message = nestedError?.message ?? payload.message;
338
338
  acc.streamErrorMessage =
339
339
  typeof message === "string" && message.trim()
340
- ? message.trim()
340
+ ? truncateString(message.trim(), MAX_EVENT_DATA_BYTES)
341
341
  : truncateString(event.data, MAX_EVENT_DATA_BYTES);
342
342
  }
343
343
  switch (event.event) {
@@ -0,0 +1,6 @@
1
+ /** Open a restrictive worker log without making proxy startup depend on it. */
2
+ export declare function openProxyWorkerLog(filename: string, logDir?: string): {
3
+ stdio: number | "ignore";
4
+ close: () => void;
5
+ error?: string;
6
+ };
@@ -0,0 +1,42 @@
1
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, statSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join } from "node:path";
4
+ /** Open a restrictive worker log without making proxy startup depend on it. */
5
+ export function openProxyWorkerLog(filename, logDir = join(homedir(), ".neurolink", "logs")) {
6
+ let fd;
7
+ try {
8
+ if (basename(filename) !== filename) {
9
+ throw new Error("worker log filename must not contain a path");
10
+ }
11
+ if (!existsSync(logDir)) {
12
+ mkdirSync(logDir, { recursive: true, mode: 0o700 });
13
+ }
14
+ else if (!statSync(logDir).isDirectory()) {
15
+ throw new Error("worker log path exists but is not a directory");
16
+ }
17
+ chmodSync(logDir, 0o700);
18
+ const path = join(logDir, filename);
19
+ fd = openSync(path, "a", 0o600);
20
+ chmodSync(path, 0o600);
21
+ return {
22
+ stdio: fd,
23
+ close: () => closeSync(fd),
24
+ };
25
+ }
26
+ catch (error) {
27
+ if (fd !== undefined) {
28
+ try {
29
+ closeSync(fd);
30
+ }
31
+ catch {
32
+ // The log is optional and startup must remain fail-open.
33
+ }
34
+ }
35
+ return {
36
+ stdio: "ignore",
37
+ close: () => undefined,
38
+ error: error instanceof Error ? error.message : String(error),
39
+ };
40
+ }
41
+ }
42
+ //# sourceMappingURL=workerLog.js.map
@@ -12,6 +12,11 @@
12
12
  * provider/model pairs (e.g. "gpt-4o" -> vertex/gemini-2.5-pro).
13
13
  */
14
14
  import type { ModelRouterInterface, ProxyRuntimeConfigProvider, RouteGroup } from "../../types/index.js";
15
+ declare function resolveStreamCancellationLifecycle(terminalStreamError: string | undefined): {
16
+ status: number;
17
+ errorType: string;
18
+ errorMessage: string;
19
+ };
15
20
  /**
16
21
  * Create OpenAI-compatible proxy routes.
17
22
  *
@@ -27,3 +32,7 @@ import type { ModelRouterInterface, ProxyRuntimeConfigProvider, RouteGroup } fro
27
32
  * @returns RouteGroup with OpenAI-compatible endpoints.
28
33
  */
29
34
  export declare function createOpenAIProxyRoutes(modelRouter?: ModelRouterInterface, basePath?: string, loopbackPort?: number, runtimeConfigProvider?: ProxyRuntimeConfigProvider): RouteGroup;
35
+ export declare const __testHooks: {
36
+ resolveStreamCancellationLifecycle: typeof resolveStreamCancellationLifecycle;
37
+ };
38
+ export {};
@@ -27,6 +27,19 @@ const LOOPBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes — long enough for slow
27
27
  // `createOpenAIProxyRoutes`'s third argument when the actual listener port is
28
28
  // known (e.g. when started from the CLI handler).
29
29
  const DEFAULT_LOOPBACK_PORT = 55669;
30
+ function resolveStreamCancellationLifecycle(terminalStreamError) {
31
+ return terminalStreamError
32
+ ? {
33
+ status: 502,
34
+ errorType: "loopback_stream_error",
35
+ errorMessage: terminalStreamError,
36
+ }
37
+ : {
38
+ status: 499,
39
+ errorType: "client_cancelled",
40
+ errorMessage: "Client cancelled the OpenAI-compatible stream",
41
+ };
42
+ }
30
43
  /**
31
44
  * Build an OpenAI-shaped error as a typed Response with the intended status.
32
45
  *
@@ -188,7 +201,8 @@ async function handleOpenAIToAnthropicBridge(args) {
188
201
  await reader.cancel(reason);
189
202
  }
190
203
  finally {
191
- await finishLifecycle(499, "client_cancelled", "Client cancelled the OpenAI-compatible stream");
204
+ const cancellation = resolveStreamCancellationLifecycle(terminalStreamError);
205
+ await finishLifecycle(cancellation.status, cancellation.errorType, cancellation.errorMessage);
192
206
  }
193
207
  },
194
208
  });
@@ -381,4 +395,5 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
381
395
  ],
382
396
  };
383
397
  }
398
+ export const __testHooks = { resolveStreamCancellationLifecycle };
384
399
  //# sourceMappingURL=openaiProxyRoutes.js.map
@@ -14,7 +14,7 @@ function writableDirectory(path) {
14
14
  if (!existsSync(path)) {
15
15
  return false;
16
16
  }
17
- accessSync(path, constants.W_OK);
17
+ accessSync(path, constants.W_OK | constants.X_OK);
18
18
  return true;
19
19
  }
20
20
  catch {
@@ -760,6 +760,7 @@ export function createClaudeToOpenAIStreamTransform(requestModel, options = {})
760
760
  finished = true;
761
761
  options.onError?.(message);
762
762
  emit(controller, serializer.emitError(message));
763
+ controller.terminate();
763
764
  return;
764
765
  }
765
766
  default:
@@ -337,7 +337,7 @@ function processEvent(acc, event) {
337
337
  const message = nestedError?.message ?? payload.message;
338
338
  acc.streamErrorMessage =
339
339
  typeof message === "string" && message.trim()
340
- ? message.trim()
340
+ ? truncateString(message.trim(), MAX_EVENT_DATA_BYTES)
341
341
  : truncateString(event.data, MAX_EVENT_DATA_BYTES);
342
342
  }
343
343
  switch (event.event) {
@@ -0,0 +1,6 @@
1
+ /** Open a restrictive worker log without making proxy startup depend on it. */
2
+ export declare function openProxyWorkerLog(filename: string, logDir?: string): {
3
+ stdio: number | "ignore";
4
+ close: () => void;
5
+ error?: string;
6
+ };
@@ -0,0 +1,41 @@
1
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, statSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join } from "node:path";
4
+ /** Open a restrictive worker log without making proxy startup depend on it. */
5
+ export function openProxyWorkerLog(filename, logDir = join(homedir(), ".neurolink", "logs")) {
6
+ let fd;
7
+ try {
8
+ if (basename(filename) !== filename) {
9
+ throw new Error("worker log filename must not contain a path");
10
+ }
11
+ if (!existsSync(logDir)) {
12
+ mkdirSync(logDir, { recursive: true, mode: 0o700 });
13
+ }
14
+ else if (!statSync(logDir).isDirectory()) {
15
+ throw new Error("worker log path exists but is not a directory");
16
+ }
17
+ chmodSync(logDir, 0o700);
18
+ const path = join(logDir, filename);
19
+ fd = openSync(path, "a", 0o600);
20
+ chmodSync(path, 0o600);
21
+ return {
22
+ stdio: fd,
23
+ close: () => closeSync(fd),
24
+ };
25
+ }
26
+ catch (error) {
27
+ if (fd !== undefined) {
28
+ try {
29
+ closeSync(fd);
30
+ }
31
+ catch {
32
+ // The log is optional and startup must remain fail-open.
33
+ }
34
+ }
35
+ return {
36
+ stdio: "ignore",
37
+ close: () => undefined,
38
+ error: error instanceof Error ? error.message : String(error),
39
+ };
40
+ }
41
+ }
@@ -12,6 +12,11 @@
12
12
  * provider/model pairs (e.g. "gpt-4o" -> vertex/gemini-2.5-pro).
13
13
  */
14
14
  import type { ModelRouterInterface, ProxyRuntimeConfigProvider, RouteGroup } from "../../types/index.js";
15
+ declare function resolveStreamCancellationLifecycle(terminalStreamError: string | undefined): {
16
+ status: number;
17
+ errorType: string;
18
+ errorMessage: string;
19
+ };
15
20
  /**
16
21
  * Create OpenAI-compatible proxy routes.
17
22
  *
@@ -27,3 +32,7 @@ import type { ModelRouterInterface, ProxyRuntimeConfigProvider, RouteGroup } fro
27
32
  * @returns RouteGroup with OpenAI-compatible endpoints.
28
33
  */
29
34
  export declare function createOpenAIProxyRoutes(modelRouter?: ModelRouterInterface, basePath?: string, loopbackPort?: number, runtimeConfigProvider?: ProxyRuntimeConfigProvider): RouteGroup;
35
+ export declare const __testHooks: {
36
+ resolveStreamCancellationLifecycle: typeof resolveStreamCancellationLifecycle;
37
+ };
38
+ export {};
@@ -27,6 +27,19 @@ const LOOPBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes — long enough for slow
27
27
  // `createOpenAIProxyRoutes`'s third argument when the actual listener port is
28
28
  // known (e.g. when started from the CLI handler).
29
29
  const DEFAULT_LOOPBACK_PORT = 55669;
30
+ function resolveStreamCancellationLifecycle(terminalStreamError) {
31
+ return terminalStreamError
32
+ ? {
33
+ status: 502,
34
+ errorType: "loopback_stream_error",
35
+ errorMessage: terminalStreamError,
36
+ }
37
+ : {
38
+ status: 499,
39
+ errorType: "client_cancelled",
40
+ errorMessage: "Client cancelled the OpenAI-compatible stream",
41
+ };
42
+ }
30
43
  /**
31
44
  * Build an OpenAI-shaped error as a typed Response with the intended status.
32
45
  *
@@ -188,7 +201,8 @@ async function handleOpenAIToAnthropicBridge(args) {
188
201
  await reader.cancel(reason);
189
202
  }
190
203
  finally {
191
- await finishLifecycle(499, "client_cancelled", "Client cancelled the OpenAI-compatible stream");
204
+ const cancellation = resolveStreamCancellationLifecycle(terminalStreamError);
205
+ await finishLifecycle(cancellation.status, cancellation.errorType, cancellation.errorMessage);
192
206
  }
193
207
  },
194
208
  });
@@ -381,3 +395,4 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
381
395
  ],
382
396
  };
383
397
  }
398
+ export const __testHooks = { resolveStreamCancellationLifecycle };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.93.1",
3
+ "version": "9.93.2",
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": {