@juspay/neurolink 9.88.1 → 9.88.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.
@@ -50,7 +50,7 @@ const ConfigSchema = z.object({
50
50
  sessionToken: z.string().optional(),
51
51
  model: z
52
52
  .string()
53
- .default("arn:aws:bedrock:us-east-2:225681119357:inference-profile/us.anthropic.claude-3-7-sonnet-20250219-v1:0"),
53
+ .default("arn:aws:bedrock:us-east-2:123456789012:inference-profile/us.anthropic.claude-3-7-sonnet-20250219-v1:0"),
54
54
  })
55
55
  .optional(),
56
56
  vertex: z
@@ -582,7 +582,7 @@ export class ConfigManager {
582
582
  type: "input",
583
583
  name: "model",
584
584
  message: "Model ARN:",
585
- default: "arn:aws:bedrock:us-east-2:225681119357:inference-profile/us.anthropic.claude-3-7-sonnet-20250219-v1:0",
585
+ default: "arn:aws:bedrock:us-east-2:123456789012:inference-profile/us.anthropic.claude-3-7-sonnet-20250219-v1:0",
586
586
  },
587
587
  ]);
588
588
  this.config.providers.bedrock = {
@@ -298,6 +298,16 @@ export type FileDetectorOptions = {
298
298
  maxSize?: number;
299
299
  timeout?: number;
300
300
  allowedTypes?: FileType[];
301
+ /**
302
+ * When set, local file paths must resolve inside this base directory;
303
+ * anything that escapes it (absolute path, `../` traversal, or a symlink
304
+ * pointing outside) is rejected. Containment is enforced on the real,
305
+ * symlink-resolved path of both the base dir and the target, so a symlink
306
+ * inside the base cannot be used to reach a file outside it. Servers that
307
+ * accept file paths from untrusted callers should set this to sandbox
308
+ * filesystem access; SDK callers loading their own files can omit it.
309
+ */
310
+ allowedBaseDir?: string;
301
311
  audioOptions?: AudioProcessorOptions;
302
312
  csvOptions?: CSVProcessorOptions;
303
313
  officeOptions?: OfficeProcessorOptions;
@@ -115,17 +115,25 @@ function isDirectorSegment(segment) {
115
115
  return false;
116
116
  }
117
117
  export function isMultimodalInput(input) {
118
- const maybeInput = input;
119
- return !!(maybeInput?.images?.length ||
120
- maybeInput?.csvFiles?.length ||
121
- maybeInput?.pdfFiles?.length ||
122
- maybeInput?.files?.length ||
123
- maybeInput?.content?.length ||
124
- maybeInput?.audioFiles?.length ||
125
- maybeInput?.videoFiles?.length ||
126
- (maybeInput?.segments?.length &&
127
- Array.isArray(maybeInput.segments) &&
128
- maybeInput.segments.every(isDirectorSegment)));
118
+ if (typeof input !== "object" || input === null) {
119
+ return false;
120
+ }
121
+ const m = input;
122
+ // Each field must be a NON-EMPTY ARRAY. Checking `.length` alone is unsafe:
123
+ // a string value (e.g. `{ images: "oops" }`) has a truthy `.length`, so the
124
+ // old guard would claim a malformed input is multimodal and then crash
125
+ // downstream when the field is iterated as an array.
126
+ const hasItems = (v) => Array.isArray(v) && v.length > 0;
127
+ return !!(hasItems(m.images) ||
128
+ hasItems(m.csvFiles) ||
129
+ hasItems(m.pdfFiles) ||
130
+ hasItems(m.files) ||
131
+ hasItems(m.content) ||
132
+ hasItems(m.audioFiles) ||
133
+ hasItems(m.videoFiles) ||
134
+ (Array.isArray(m.segments) &&
135
+ m.segments.length > 0 &&
136
+ m.segments.every(isDirectorSegment)));
129
137
  }
130
138
  /**
131
139
  * Type guard to check if message content is multimodal (array)
@@ -3,7 +3,8 @@
3
3
  * Centralized file detection for all multimodal file types
4
4
  * Uses multi-strategy approach for reliable type identification
5
5
  */
6
- import { readFile, stat } from "fs/promises";
6
+ import { open, readFile, realpath } from "fs/promises";
7
+ import { isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
7
8
  import { getGlobalDispatcher, interceptors, request } from "undici";
8
9
  // Lazy-loaded processor singletons — avoids loading heavy media deps
9
10
  // (mediabunny, fluent-ffmpeg, music-metadata, adm-zip) on every generate() call.
@@ -585,7 +586,9 @@ export class FileDetector {
585
586
  return result;
586
587
  }
587
588
  }
588
- logger.warn(`[FileDetector] Low confidence: ${best?.type ?? "unknown"} (${best?.metadata.confidence ?? 0}%)`);
589
+ // Below-threshold detection is the common case for any file under the
590
+ // ContentHeuristic ceiling — a debug detail, not a warning-worthy anomaly.
591
+ logger.debug(`[FileDetector] Best-effort type below threshold: ${best?.type ?? "unknown"} (${best?.metadata.confidence ?? 0}%, threshold ${confidenceThreshold}%)`);
589
592
  return best;
590
593
  }
591
594
  /**
@@ -1342,7 +1345,7 @@ export class FileDetector {
1342
1345
  bodyTimeout: timeout,
1343
1346
  });
1344
1347
  if (response.statusCode !== 200) {
1345
- throw new Error(`HTTP ${response.statusCode}`);
1348
+ throw new Error(`HTTP ${response.statusCode} fetching ${url}`);
1346
1349
  }
1347
1350
  const chunks = [];
1348
1351
  let totalSize = 0;
@@ -1359,16 +1362,49 @@ export class FileDetector {
1359
1362
  /**
1360
1363
  * Load file from filesystem path
1361
1364
  */
1362
- static async loadFromPath(path, options) {
1365
+ static async loadFromPath(filePath, options) {
1363
1366
  const maxSize = options?.maxSize || 200 * 1024 * 1024; // 200MB default (matches Curator memory-safety cap)
1364
- const statInfo = await stat(path);
1365
- if (!statInfo.isFile()) {
1366
- throw new Error("Not a file");
1367
+ // Reject NUL-byte injection outright (a classic path-truncation vector).
1368
+ if (filePath.includes("\0")) {
1369
+ throw new Error("Invalid file path: contains a null byte");
1370
+ }
1371
+ // Optional sandbox: when a base dir is configured (servers accepting paths
1372
+ // from untrusted callers), reject anything that resolves outside it. Real
1373
+ // paths are resolved (symlinks followed) on BOTH sides so a symlink inside
1374
+ // the base dir pointing outside cannot bypass containment. The
1375
+ // path.relative check (not a string prefix) correctly handles the root dir
1376
+ // ("/") and sibling-prefix ("/app" vs "/app-evil") edge cases.
1377
+ if (options?.allowedBaseDir) {
1378
+ let base;
1379
+ let real;
1380
+ try {
1381
+ base = await realpath(resolvePath(options.allowedBaseDir));
1382
+ real = await realpath(filePath);
1383
+ }
1384
+ catch {
1385
+ throw new Error(`Access denied: "${filePath}" could not be resolved within the allowed base directory`);
1386
+ }
1387
+ const rel = relativePath(base, real);
1388
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolutePath(rel)) {
1389
+ throw new Error(`Access denied: "${filePath}" resolves outside the allowed base directory`);
1390
+ }
1391
+ }
1392
+ // Open a handle and stat/read through the SAME descriptor so a symlink
1393
+ // swap between the size check and the read cannot occur (TOCTOU).
1394
+ const handle = await open(filePath, "r");
1395
+ try {
1396
+ const statInfo = await handle.stat();
1397
+ if (!statInfo.isFile()) {
1398
+ throw new Error(`Not a file: ${filePath}`);
1399
+ }
1400
+ if (statInfo.size > maxSize) {
1401
+ throw new Error(`File too large: ${filePath} is ${formatFileSize(statInfo.size)} (max: ${formatFileSize(maxSize)})`);
1402
+ }
1403
+ return await handle.readFile();
1367
1404
  }
1368
- if (statInfo.size > maxSize) {
1369
- throw new Error(`File too large: ${formatFileSize(statInfo.size)} (max: ${formatFileSize(maxSize)})`);
1405
+ finally {
1406
+ await handle.close();
1370
1407
  }
1371
- return await readFile(path);
1372
1408
  }
1373
1409
  /**
1374
1410
  * Load file from data URI
@@ -1376,7 +1412,7 @@ export class FileDetector {
1376
1412
  static loadFromDataURI(dataUri) {
1377
1413
  const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
1378
1414
  if (!match) {
1379
- throw new Error("Invalid data URI format");
1415
+ throw new Error(`Invalid data URI format (expected "data:<mime>;base64,<data>"): "${dataUri.slice(0, 32)}…"`);
1380
1416
  }
1381
1417
  return Buffer.from(match[2], "base64");
1382
1418
  }
@@ -298,6 +298,16 @@ export type FileDetectorOptions = {
298
298
  maxSize?: number;
299
299
  timeout?: number;
300
300
  allowedTypes?: FileType[];
301
+ /**
302
+ * When set, local file paths must resolve inside this base directory;
303
+ * anything that escapes it (absolute path, `../` traversal, or a symlink
304
+ * pointing outside) is rejected. Containment is enforced on the real,
305
+ * symlink-resolved path of both the base dir and the target, so a symlink
306
+ * inside the base cannot be used to reach a file outside it. Servers that
307
+ * accept file paths from untrusted callers should set this to sandbox
308
+ * filesystem access; SDK callers loading their own files can omit it.
309
+ */
310
+ allowedBaseDir?: string;
301
311
  audioOptions?: AudioProcessorOptions;
302
312
  csvOptions?: CSVProcessorOptions;
303
313
  officeOptions?: OfficeProcessorOptions;
@@ -115,17 +115,25 @@ function isDirectorSegment(segment) {
115
115
  return false;
116
116
  }
117
117
  export function isMultimodalInput(input) {
118
- const maybeInput = input;
119
- return !!(maybeInput?.images?.length ||
120
- maybeInput?.csvFiles?.length ||
121
- maybeInput?.pdfFiles?.length ||
122
- maybeInput?.files?.length ||
123
- maybeInput?.content?.length ||
124
- maybeInput?.audioFiles?.length ||
125
- maybeInput?.videoFiles?.length ||
126
- (maybeInput?.segments?.length &&
127
- Array.isArray(maybeInput.segments) &&
128
- maybeInput.segments.every(isDirectorSegment)));
118
+ if (typeof input !== "object" || input === null) {
119
+ return false;
120
+ }
121
+ const m = input;
122
+ // Each field must be a NON-EMPTY ARRAY. Checking `.length` alone is unsafe:
123
+ // a string value (e.g. `{ images: "oops" }`) has a truthy `.length`, so the
124
+ // old guard would claim a malformed input is multimodal and then crash
125
+ // downstream when the field is iterated as an array.
126
+ const hasItems = (v) => Array.isArray(v) && v.length > 0;
127
+ return !!(hasItems(m.images) ||
128
+ hasItems(m.csvFiles) ||
129
+ hasItems(m.pdfFiles) ||
130
+ hasItems(m.files) ||
131
+ hasItems(m.content) ||
132
+ hasItems(m.audioFiles) ||
133
+ hasItems(m.videoFiles) ||
134
+ (Array.isArray(m.segments) &&
135
+ m.segments.length > 0 &&
136
+ m.segments.every(isDirectorSegment)));
129
137
  }
130
138
  /**
131
139
  * Type guard to check if message content is multimodal (array)
@@ -3,7 +3,8 @@
3
3
  * Centralized file detection for all multimodal file types
4
4
  * Uses multi-strategy approach for reliable type identification
5
5
  */
6
- import { readFile, stat } from "fs/promises";
6
+ import { open, readFile, realpath } from "fs/promises";
7
+ import { isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
7
8
  import { getGlobalDispatcher, interceptors, request } from "undici";
8
9
  // Lazy-loaded processor singletons — avoids loading heavy media deps
9
10
  // (mediabunny, fluent-ffmpeg, music-metadata, adm-zip) on every generate() call.
@@ -585,7 +586,9 @@ export class FileDetector {
585
586
  return result;
586
587
  }
587
588
  }
588
- logger.warn(`[FileDetector] Low confidence: ${best?.type ?? "unknown"} (${best?.metadata.confidence ?? 0}%)`);
589
+ // Below-threshold detection is the common case for any file under the
590
+ // ContentHeuristic ceiling — a debug detail, not a warning-worthy anomaly.
591
+ logger.debug(`[FileDetector] Best-effort type below threshold: ${best?.type ?? "unknown"} (${best?.metadata.confidence ?? 0}%, threshold ${confidenceThreshold}%)`);
589
592
  return best;
590
593
  }
591
594
  /**
@@ -1342,7 +1345,7 @@ export class FileDetector {
1342
1345
  bodyTimeout: timeout,
1343
1346
  });
1344
1347
  if (response.statusCode !== 200) {
1345
- throw new Error(`HTTP ${response.statusCode}`);
1348
+ throw new Error(`HTTP ${response.statusCode} fetching ${url}`);
1346
1349
  }
1347
1350
  const chunks = [];
1348
1351
  let totalSize = 0;
@@ -1359,16 +1362,49 @@ export class FileDetector {
1359
1362
  /**
1360
1363
  * Load file from filesystem path
1361
1364
  */
1362
- static async loadFromPath(path, options) {
1365
+ static async loadFromPath(filePath, options) {
1363
1366
  const maxSize = options?.maxSize || 200 * 1024 * 1024; // 200MB default (matches Curator memory-safety cap)
1364
- const statInfo = await stat(path);
1365
- if (!statInfo.isFile()) {
1366
- throw new Error("Not a file");
1367
+ // Reject NUL-byte injection outright (a classic path-truncation vector).
1368
+ if (filePath.includes("\0")) {
1369
+ throw new Error("Invalid file path: contains a null byte");
1370
+ }
1371
+ // Optional sandbox: when a base dir is configured (servers accepting paths
1372
+ // from untrusted callers), reject anything that resolves outside it. Real
1373
+ // paths are resolved (symlinks followed) on BOTH sides so a symlink inside
1374
+ // the base dir pointing outside cannot bypass containment. The
1375
+ // path.relative check (not a string prefix) correctly handles the root dir
1376
+ // ("/") and sibling-prefix ("/app" vs "/app-evil") edge cases.
1377
+ if (options?.allowedBaseDir) {
1378
+ let base;
1379
+ let real;
1380
+ try {
1381
+ base = await realpath(resolvePath(options.allowedBaseDir));
1382
+ real = await realpath(filePath);
1383
+ }
1384
+ catch {
1385
+ throw new Error(`Access denied: "${filePath}" could not be resolved within the allowed base directory`);
1386
+ }
1387
+ const rel = relativePath(base, real);
1388
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolutePath(rel)) {
1389
+ throw new Error(`Access denied: "${filePath}" resolves outside the allowed base directory`);
1390
+ }
1391
+ }
1392
+ // Open a handle and stat/read through the SAME descriptor so a symlink
1393
+ // swap between the size check and the read cannot occur (TOCTOU).
1394
+ const handle = await open(filePath, "r");
1395
+ try {
1396
+ const statInfo = await handle.stat();
1397
+ if (!statInfo.isFile()) {
1398
+ throw new Error(`Not a file: ${filePath}`);
1399
+ }
1400
+ if (statInfo.size > maxSize) {
1401
+ throw new Error(`File too large: ${filePath} is ${formatFileSize(statInfo.size)} (max: ${formatFileSize(maxSize)})`);
1402
+ }
1403
+ return await handle.readFile();
1367
1404
  }
1368
- if (statInfo.size > maxSize) {
1369
- throw new Error(`File too large: ${formatFileSize(statInfo.size)} (max: ${formatFileSize(maxSize)})`);
1405
+ finally {
1406
+ await handle.close();
1370
1407
  }
1371
- return await readFile(path);
1372
1408
  }
1373
1409
  /**
1374
1410
  * Load file from data URI
@@ -1376,7 +1412,7 @@ export class FileDetector {
1376
1412
  static loadFromDataURI(dataUri) {
1377
1413
  const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
1378
1414
  if (!match) {
1379
- throw new Error("Invalid data URI format");
1415
+ throw new Error(`Invalid data URI format (expected "data:<mime>;base64,<data>"): "${dataUri.slice(0, 32)}…"`);
1380
1416
  }
1381
1417
  return Buffer.from(match[2], "base64");
1382
1418
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.88.1",
3
+ "version": "9.88.3",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (58+ servers), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {