@juspay/neurolink 9.44.1 → 9.48.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.
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { spawn } from "node:child_process";
13
13
  import { homedir } from "node:os";
14
- import { join, resolve } from "node:path";
14
+ import { dirname, join, resolve } from "node:path";
15
15
  import chalk from "chalk";
16
16
  import ora from "ora";
17
17
  import { buildProxyHealthResponse, createProxyReadinessState, markProxyReady, waitForProxyReadiness, } from "../../lib/proxy/proxyHealth.js";
@@ -1516,6 +1516,29 @@ function escapeXml(s) {
1516
1516
  .replace(/"/g, """)
1517
1517
  .replace(/'/g, "'");
1518
1518
  }
1519
+ /**
1520
+ * Build a PATH for the launchd plist that includes the current Node/pnpm
1521
+ * bin directories so the guard process can find npm/pnpm for update checks.
1522
+ */
1523
+ function buildLaunchdPath() {
1524
+ const fallback = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin";
1525
+ const nodeDir = dirname(process.execPath);
1526
+ const segments = new Set();
1527
+ // Add the directory containing the Node binary that launched this process
1528
+ if (nodeDir && nodeDir !== ".") {
1529
+ segments.add(nodeDir);
1530
+ }
1531
+ // Add pnpm home if available (e.g., ~/.local/share/pnpm)
1532
+ const pnpmHome = process.env.PNPM_HOME;
1533
+ if (pnpmHome) {
1534
+ segments.add(pnpmHome);
1535
+ }
1536
+ // Add the standard system paths
1537
+ for (const p of fallback.split(":")) {
1538
+ segments.add(p);
1539
+ }
1540
+ return [...segments].join(":");
1541
+ }
1519
1542
  function buildPlist(port, host, envFile, configFile) {
1520
1543
  const nodeExec = escapeXml(process.execPath);
1521
1544
  const entryScript = escapeXml(process.argv[1] ?? join(__dirname, "..", "index.js"));
@@ -1573,7 +1596,7 @@ ${configArgs}
1573
1596
  <key>EnvironmentVariables</key>
1574
1597
  <dict>
1575
1598
  <key>PATH</key>
1576
- <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
1599
+ <string>${buildLaunchdPath()}</string>
1577
1600
  <key>HOME</key>
1578
1601
  <string>${homedir()}</string>
1579
1602
  </dict>
@@ -124,7 +124,10 @@ export class CLICommandFactory {
124
124
  type: "string",
125
125
  choices: ["raw", "markdown", "json"],
126
126
  default: "raw",
127
- description: "CSV output format (raw recommended for large files)",
127
+ description: "CSV output format:\n" +
128
+ " • raw: Plain CSV text (fastest, minimal tokens, best for large files)\n" +
129
+ " • markdown: Formatted table (readable, best for small files <100 rows)\n" +
130
+ " • json: Structured JSON array (best for programmatic use, higher tokens)",
128
131
  },
129
132
  model: {
130
133
  type: "string",
@@ -1081,6 +1084,9 @@ export class CLICommandFactory {
1081
1084
  .example('$0 generate "Futuristic city" --model gemini-2.5-flash-image', "Generate an image")
1082
1085
  .example('$0 generate "Mountain landscape" --model gemini-2.5-flash-image --imageOutput ./my-images/mountain.png', "Generate image with custom path")
1083
1086
  .example('$0 generate "Describe this video" --video path/to/video.mp4', "Analyze video content")
1087
+ .example('$0 generate "Analyze sales" --csv data.csv --csv-format raw', "CSV with raw format (fast, minimal tokens)")
1088
+ .example('$0 generate "Summarize data" --csv small.csv --csv-format markdown', "CSV with markdown table (readable)")
1089
+ .example('$0 generate "Process data" --csv records.csv --csv-format json', "CSV with JSON format (structured)")
1084
1090
  .example('$0 generate "Product showcase video" --image ./product.jpg --outputMode video --videoOutput ./output.mp4', "Generate video from image")
1085
1091
  .example('$0 generate "Smooth camera movement" --image ./input.jpg --provider vertex --model veo-3.1-generate-001 --outputMode video --videoResolution 720p --videoLength 6 --videoAspectRatio 16:9 --videoOutput ./output.mp4', "Video generation with full options")
1086
1092
  .example('$0 generate "AI in Healthcare" --pptPages 10', "Generate a PowerPoint presentation")
@@ -148,7 +148,8 @@ export class TokenStore {
148
148
  ? this.obfuscate(JSON.stringify(storageData))
149
149
  : JSON.stringify(storageData, null, 2);
150
150
  // Write to temporary file first for atomic operation
151
- const tempPath = `${this.storagePath}.tmp`;
151
+ // Use PID-scoped temp file to avoid cross-process race conditions
152
+ const tempPath = `${this.storagePath}.tmp.${process.pid}`;
152
153
  await writeFile(tempPath, content, "utf-8");
153
154
  // Set restrictive permissions before moving to final location
154
155
  await chmod(tempPath, TokenStore.FILE_PERMISSIONS);
@@ -681,7 +682,8 @@ export class TokenStore {
681
682
  const content = this.encryptionEnabled
682
683
  ? this.obfuscate(JSON.stringify(data))
683
684
  : JSON.stringify(data, null, 2);
684
- const tmpPath = `${this.storagePath}.tmp`;
685
+ // Use PID-scoped temp file to avoid cross-process race conditions
686
+ const tmpPath = `${this.storagePath}.tmp.${process.pid}`;
685
687
  await writeFile(tmpPath, content, "utf-8");
686
688
  await chmod(tmpPath, TokenStore.FILE_PERMISSIONS);
687
689
  await rename(tmpPath, this.storagePath);
@@ -16,7 +16,7 @@ export type { BatchProcessingSummary, ExcelWorksheet, FailedFileInfo, FileInfo,
16
16
  export { PROCESSOR_PRIORITIES } from "../processors/base/types.js";
17
17
  export type { ActionAWSConfig, ActionCommentResult, ActionEvaluation, ActionExecutionResult, ActionGoogleCloudConfig, ActionInputs, ActionInputValidation, ActionMultimodalInputs, ActionOutput, ActionProviderKeys, ActionThinkingConfig, ActionTokenUsage, CliAnalytics, CliEvaluation, CliResponse, CliTokenUsage, ProviderKeyMapping, } from "./actionTypes.js";
18
18
  export type { AnalyticsData, AnalyticsErrorInfo, PerformanceMetrics, StreamAnalyticsData, TokenUsage, } from "./analytics.js";
19
- export * from "./content.js";
19
+ export * from "./multimodal.js";
20
20
  export type { DomainConfig, DomainConfigOptions, DomainEvaluationCriteria, DomainTemplate, DomainType, DomainValidationRule, } from "./domainTypes.js";
21
21
  export * from "./evaluation.js";
22
22
  export * from "./evaluationProviders.js";
@@ -14,8 +14,8 @@ export * from "./taskClassificationTypes.js";
14
14
  // Tool system types
15
15
  export * from "./tools.js";
16
16
  export { PROCESSOR_PRIORITIES } from "../processors/base/types.js";
17
- // Content types for multimodal support (includes multimodal re-exports for backward compatibility)
18
- export * from "./content.js";
17
+ // Content types for multimodal support (direct export from canonical source)
18
+ export * from "./multimodal.js";
19
19
  // Evaluation types - NEW
20
20
  export * from "./evaluation.js";
21
21
  // Evaluation provider types - NEW
@@ -79,6 +79,11 @@ export declare class ImageProcessor {
79
79
  */
80
80
  static processImage(image: Buffer | string, provider: string, model?: string): ProcessedImage;
81
81
  }
82
+ /**
83
+ * Whitelist of valid image file extensions (lowercase, no dots).
84
+ * Used to validate file extensions against a known set of image formats.
85
+ */
86
+ export declare const VALID_IMAGE_EXTENSIONS: string[];
82
87
  /**
83
88
  * Utility functions for image handling
84
89
  */
@@ -96,9 +101,27 @@ export declare const imageUtils: {
96
101
  */
97
102
  isBase64: (str: string) => boolean;
98
103
  /**
99
- * Extract file extension from filename or URL
104
+ * Extract file extension from filename or URL.
105
+ * Strips query strings and fragments before matching so that
106
+ * "image.jpg?v=1" correctly returns "jpg".
107
+ * Returns null if no extension is found or if the extension
108
+ * contains non-alphanumeric characters.
100
109
  */
101
110
  getFileExtension: (filename: string) => string | null;
111
+ /**
112
+ * Validate that an extension is a recognised image format.
113
+ * Case-insensitive; rejects extensions with special characters.
114
+ */
115
+ isValidImageExtension: (extension: string) => boolean;
116
+ /**
117
+ * Extract and validate image file extension from a filename or URL.
118
+ * Returns null if the extension is missing or not a recognised image format.
119
+ *
120
+ * Security note: the last extension is used, so "malware.exe.jpg" returns
121
+ * "jpg". Callers should apply additional checks (e.g. content inspection)
122
+ * where double-extension attacks are a concern.
123
+ */
124
+ getValidatedImageExtension: (filename: string) => string | null;
102
125
  /**
103
126
  * Convert file size to human readable format
104
127
  */
@@ -364,11 +364,58 @@ export class ImageProcessor {
364
364
  const height = buffer.readUInt32BE(20);
365
365
  return { width, height };
366
366
  }
367
- // Basic JPEG dimension extraction (simplified)
367
+ // JPEG dimension extraction via SOF marker parsing
368
368
  if (buffer.length >= 4 && buffer[0] === 0xff && buffer[1] === 0xd8) {
369
- // This is a very basic implementation
370
- // For production, consider using a proper image library
371
- return null;
369
+ // Search for SOF0 (0xFFC0) or SOF2 (0xFFC2) markers
370
+ let offset = 2;
371
+ while (offset < buffer.length - 1) {
372
+ // Find next marker (0xFF followed by non-zero, non-0xFF byte)
373
+ if (buffer[offset] !== 0xff) {
374
+ offset++;
375
+ continue;
376
+ }
377
+ // Skip any padding 0xFF bytes
378
+ while (offset < buffer.length && buffer[offset] === 0xff) {
379
+ offset++;
380
+ }
381
+ if (offset >= buffer.length) {
382
+ break;
383
+ }
384
+ const marker = buffer[offset];
385
+ offset++;
386
+ // Check for SOF0 (0xC0 - baseline DCT) and SOF2 (0xC2 - progressive DCT)
387
+ // These are the most common JPEG encoding modes
388
+ if (marker === 0xc0 || marker === 0xc2) {
389
+ // SOF marker found: length (2 bytes) + precision (1 byte) + height (2 bytes) + width (2 bytes)
390
+ if (offset + 7 > buffer.length) {
391
+ break; // Truncated file
392
+ }
393
+ const height = buffer.readUInt16BE(offset + 3);
394
+ const width = buffer.readUInt16BE(offset + 5);
395
+ return { width, height };
396
+ }
397
+ // Skip this marker's segment (except for markers without length)
398
+ if (marker === 0xd0 ||
399
+ marker === 0xd1 ||
400
+ marker === 0xd2 ||
401
+ marker === 0xd3 ||
402
+ marker === 0xd4 ||
403
+ marker === 0xd5 ||
404
+ marker === 0xd6 ||
405
+ marker === 0xd7 ||
406
+ marker === 0xd8 ||
407
+ marker === 0xd9 ||
408
+ marker === 0x01) {
409
+ // RST0-RST7, SOI, EOI, TEM - no length field
410
+ continue;
411
+ }
412
+ if (offset + 2 > buffer.length) {
413
+ break; // Truncated file
414
+ }
415
+ const segmentLength = buffer.readUInt16BE(offset);
416
+ offset += segmentLength;
417
+ }
418
+ return null; // No SOF marker found
372
419
  }
373
420
  return null;
374
421
  }
@@ -437,6 +484,30 @@ export class ImageProcessor {
437
484
  }
438
485
  }
439
486
  }
487
+ /**
488
+ * Whitelist of valid image file extensions (lowercase, no dots).
489
+ * Used to validate file extensions against a known set of image formats.
490
+ */
491
+ export const VALID_IMAGE_EXTENSIONS = [
492
+ "jpg",
493
+ "jpeg",
494
+ "png",
495
+ "gif",
496
+ "webp",
497
+ "bmp",
498
+ "tiff",
499
+ "tif",
500
+ "svg",
501
+ "avif",
502
+ "ico",
503
+ "heic",
504
+ "heif",
505
+ ];
506
+ /**
507
+ * Set of valid image extensions for O(1) lookup.
508
+ * @internal
509
+ */
510
+ const VALID_IMAGE_EXTENSIONS_SET = new Set(VALID_IMAGE_EXTENSIONS);
440
511
  /**
441
512
  * Utility functions for image handling
442
513
  */
@@ -466,11 +537,52 @@ export const imageUtils = {
466
537
  */
467
538
  isBase64: (str) => imageUtils.isValidBase64(str),
468
539
  /**
469
- * Extract file extension from filename or URL
540
+ * Extract file extension from filename or URL.
541
+ * Strips query strings and fragments before matching so that
542
+ * "image.jpg?v=1" correctly returns "jpg".
543
+ * Returns null if no extension is found or if the extension
544
+ * contains non-alphanumeric characters.
470
545
  */
471
546
  getFileExtension: (filename) => {
472
- const match = filename.match(/\.([^.]+)$/);
473
- return match ? match[1].toLowerCase() : null;
547
+ const sanitized = filename.split(/[?#]/)[0];
548
+ const match = sanitized.match(/\.([^.]+)$/);
549
+ if (!match) {
550
+ return null;
551
+ }
552
+ const extension = match[1].toLowerCase();
553
+ if (!/^[a-z0-9]+$/.test(extension)) {
554
+ return null;
555
+ }
556
+ return extension;
557
+ },
558
+ /**
559
+ * Validate that an extension is a recognised image format.
560
+ * Case-insensitive; rejects extensions with special characters.
561
+ */
562
+ isValidImageExtension: (extension) => {
563
+ if (!extension || typeof extension !== "string") {
564
+ return false;
565
+ }
566
+ const normalizedExt = extension.toLowerCase();
567
+ if (!/^[a-z0-9]+$/.test(normalizedExt)) {
568
+ return false;
569
+ }
570
+ return VALID_IMAGE_EXTENSIONS_SET.has(normalizedExt);
571
+ },
572
+ /**
573
+ * Extract and validate image file extension from a filename or URL.
574
+ * Returns null if the extension is missing or not a recognised image format.
575
+ *
576
+ * Security note: the last extension is used, so "malware.exe.jpg" returns
577
+ * "jpg". Callers should apply additional checks (e.g. content inspection)
578
+ * where double-extension attacks are a concern.
579
+ */
580
+ getValidatedImageExtension: (filename) => {
581
+ const extension = imageUtils.getFileExtension(filename);
582
+ if (!extension) {
583
+ return null;
584
+ }
585
+ return imageUtils.isValidImageExtension(extension) ? extension : null;
474
586
  },
475
587
  /**
476
588
  * Convert file size to human readable format
@@ -576,7 +688,11 @@ export const imageUtils = {
576
688
  if (!/^image\//i.test(contentType)) {
577
689
  throw new Error(`Unsupported content-type: ${contentType || "unknown"}`);
578
690
  }
579
- const len = Number(response.headers.get("content-length") || 0);
691
+ const contentLengthHeader = response.headers.get("content-length");
692
+ const len = Number(contentLengthHeader || 0);
693
+ if (contentLengthHeader !== null && len === 0) {
694
+ throw new Error("Empty response: content-length is 0");
695
+ }
580
696
  if (len && len > maxBytes) {
581
697
  throw new Error(`Content too large: ${len} bytes`);
582
698
  }
@@ -1127,15 +1127,27 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1127
1127
  async function convertContentToProviderFormat(content, provider, _model) {
1128
1128
  const textContent = content.find((c) => c.type === "text");
1129
1129
  const imageContent = content.filter((c) => c.type === "image");
1130
- if (!textContent) {
1131
- throw new Error("Multimodal content must include at least one text element");
1132
- }
1133
- if (imageContent.length === 0) {
1134
- return textContent.text;
1130
+ const pdfContent = content.filter((c) => c.type === "pdf");
1131
+ // Allow empty text when multimodal content is present (enables image-only or PDF-only queries)
1132
+ const text = textContent?.text || "";
1133
+ const hasMultimodal = imageContent.length > 0 || pdfContent.length > 0;
1134
+ // Validate that we have at least some content
1135
+ if (!hasMultimodal && !text) {
1136
+ throw new Error("Content must include either text or multimodal content");
1137
+ }
1138
+ // Text-only case
1139
+ if (imageContent.length === 0 && pdfContent.length === 0) {
1140
+ return text;
1135
1141
  }
1136
1142
  // Extract images as Buffer | string array
1137
1143
  const images = imageContent.map((img) => img.data);
1138
- return await convertSimpleImagesToProviderFormat(textContent.text, images, provider, _model);
1144
+ // Extract PDFs in the expected format
1145
+ const pdfFiles = pdfContent.map((pdf) => ({
1146
+ buffer: typeof pdf.data === "string" ? Buffer.from(pdf.data, "base64") : pdf.data,
1147
+ filename: pdf.metadata?.filename || "document.pdf",
1148
+ pageCount: pdf.metadata?.pages ?? null,
1149
+ }));
1150
+ return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model);
1139
1151
  }
1140
1152
  /**
1141
1153
  * Check if a string is an internet URL
@@ -16,7 +16,7 @@ export type { BatchProcessingSummary, ExcelWorksheet, FailedFileInfo, FileInfo,
16
16
  export { PROCESSOR_PRIORITIES } from "../processors/base/types.js";
17
17
  export type { ActionAWSConfig, ActionCommentResult, ActionEvaluation, ActionExecutionResult, ActionGoogleCloudConfig, ActionInputs, ActionInputValidation, ActionMultimodalInputs, ActionOutput, ActionProviderKeys, ActionThinkingConfig, ActionTokenUsage, CliAnalytics, CliEvaluation, CliResponse, CliTokenUsage, ProviderKeyMapping, } from "./actionTypes.js";
18
18
  export type { AnalyticsData, AnalyticsErrorInfo, PerformanceMetrics, StreamAnalyticsData, TokenUsage, } from "./analytics.js";
19
- export * from "./content.js";
19
+ export * from "./multimodal.js";
20
20
  export type { DomainConfig, DomainConfigOptions, DomainEvaluationCriteria, DomainTemplate, DomainType, DomainValidationRule, } from "./domainTypes.js";
21
21
  export * from "./evaluation.js";
22
22
  export * from "./evaluationProviders.js";
@@ -14,8 +14,8 @@ export * from "./taskClassificationTypes.js";
14
14
  // Tool system types
15
15
  export * from "./tools.js";
16
16
  export { PROCESSOR_PRIORITIES } from "../processors/base/types.js";
17
- // Content types for multimodal support (includes multimodal re-exports for backward compatibility)
18
- export * from "./content.js";
17
+ // Content types for multimodal support (direct export from canonical source)
18
+ export * from "./multimodal.js";
19
19
  // Evaluation types - NEW
20
20
  export * from "./evaluation.js";
21
21
  // Evaluation provider types - NEW
@@ -79,6 +79,11 @@ export declare class ImageProcessor {
79
79
  */
80
80
  static processImage(image: Buffer | string, provider: string, model?: string): ProcessedImage;
81
81
  }
82
+ /**
83
+ * Whitelist of valid image file extensions (lowercase, no dots).
84
+ * Used to validate file extensions against a known set of image formats.
85
+ */
86
+ export declare const VALID_IMAGE_EXTENSIONS: string[];
82
87
  /**
83
88
  * Utility functions for image handling
84
89
  */
@@ -96,9 +101,27 @@ export declare const imageUtils: {
96
101
  */
97
102
  isBase64: (str: string) => boolean;
98
103
  /**
99
- * Extract file extension from filename or URL
104
+ * Extract file extension from filename or URL.
105
+ * Strips query strings and fragments before matching so that
106
+ * "image.jpg?v=1" correctly returns "jpg".
107
+ * Returns null if no extension is found or if the extension
108
+ * contains non-alphanumeric characters.
100
109
  */
101
110
  getFileExtension: (filename: string) => string | null;
111
+ /**
112
+ * Validate that an extension is a recognised image format.
113
+ * Case-insensitive; rejects extensions with special characters.
114
+ */
115
+ isValidImageExtension: (extension: string) => boolean;
116
+ /**
117
+ * Extract and validate image file extension from a filename or URL.
118
+ * Returns null if the extension is missing or not a recognised image format.
119
+ *
120
+ * Security note: the last extension is used, so "malware.exe.jpg" returns
121
+ * "jpg". Callers should apply additional checks (e.g. content inspection)
122
+ * where double-extension attacks are a concern.
123
+ */
124
+ getValidatedImageExtension: (filename: string) => string | null;
102
125
  /**
103
126
  * Convert file size to human readable format
104
127
  */
@@ -364,11 +364,58 @@ export class ImageProcessor {
364
364
  const height = buffer.readUInt32BE(20);
365
365
  return { width, height };
366
366
  }
367
- // Basic JPEG dimension extraction (simplified)
367
+ // JPEG dimension extraction via SOF marker parsing
368
368
  if (buffer.length >= 4 && buffer[0] === 0xff && buffer[1] === 0xd8) {
369
- // This is a very basic implementation
370
- // For production, consider using a proper image library
371
- return null;
369
+ // Search for SOF0 (0xFFC0) or SOF2 (0xFFC2) markers
370
+ let offset = 2;
371
+ while (offset < buffer.length - 1) {
372
+ // Find next marker (0xFF followed by non-zero, non-0xFF byte)
373
+ if (buffer[offset] !== 0xff) {
374
+ offset++;
375
+ continue;
376
+ }
377
+ // Skip any padding 0xFF bytes
378
+ while (offset < buffer.length && buffer[offset] === 0xff) {
379
+ offset++;
380
+ }
381
+ if (offset >= buffer.length) {
382
+ break;
383
+ }
384
+ const marker = buffer[offset];
385
+ offset++;
386
+ // Check for SOF0 (0xC0 - baseline DCT) and SOF2 (0xC2 - progressive DCT)
387
+ // These are the most common JPEG encoding modes
388
+ if (marker === 0xc0 || marker === 0xc2) {
389
+ // SOF marker found: length (2 bytes) + precision (1 byte) + height (2 bytes) + width (2 bytes)
390
+ if (offset + 7 > buffer.length) {
391
+ break; // Truncated file
392
+ }
393
+ const height = buffer.readUInt16BE(offset + 3);
394
+ const width = buffer.readUInt16BE(offset + 5);
395
+ return { width, height };
396
+ }
397
+ // Skip this marker's segment (except for markers without length)
398
+ if (marker === 0xd0 ||
399
+ marker === 0xd1 ||
400
+ marker === 0xd2 ||
401
+ marker === 0xd3 ||
402
+ marker === 0xd4 ||
403
+ marker === 0xd5 ||
404
+ marker === 0xd6 ||
405
+ marker === 0xd7 ||
406
+ marker === 0xd8 ||
407
+ marker === 0xd9 ||
408
+ marker === 0x01) {
409
+ // RST0-RST7, SOI, EOI, TEM - no length field
410
+ continue;
411
+ }
412
+ if (offset + 2 > buffer.length) {
413
+ break; // Truncated file
414
+ }
415
+ const segmentLength = buffer.readUInt16BE(offset);
416
+ offset += segmentLength;
417
+ }
418
+ return null; // No SOF marker found
372
419
  }
373
420
  return null;
374
421
  }
@@ -437,6 +484,30 @@ export class ImageProcessor {
437
484
  }
438
485
  }
439
486
  }
487
+ /**
488
+ * Whitelist of valid image file extensions (lowercase, no dots).
489
+ * Used to validate file extensions against a known set of image formats.
490
+ */
491
+ export const VALID_IMAGE_EXTENSIONS = [
492
+ "jpg",
493
+ "jpeg",
494
+ "png",
495
+ "gif",
496
+ "webp",
497
+ "bmp",
498
+ "tiff",
499
+ "tif",
500
+ "svg",
501
+ "avif",
502
+ "ico",
503
+ "heic",
504
+ "heif",
505
+ ];
506
+ /**
507
+ * Set of valid image extensions for O(1) lookup.
508
+ * @internal
509
+ */
510
+ const VALID_IMAGE_EXTENSIONS_SET = new Set(VALID_IMAGE_EXTENSIONS);
440
511
  /**
441
512
  * Utility functions for image handling
442
513
  */
@@ -466,11 +537,52 @@ export const imageUtils = {
466
537
  */
467
538
  isBase64: (str) => imageUtils.isValidBase64(str),
468
539
  /**
469
- * Extract file extension from filename or URL
540
+ * Extract file extension from filename or URL.
541
+ * Strips query strings and fragments before matching so that
542
+ * "image.jpg?v=1" correctly returns "jpg".
543
+ * Returns null if no extension is found or if the extension
544
+ * contains non-alphanumeric characters.
470
545
  */
471
546
  getFileExtension: (filename) => {
472
- const match = filename.match(/\.([^.]+)$/);
473
- return match ? match[1].toLowerCase() : null;
547
+ const sanitized = filename.split(/[?#]/)[0];
548
+ const match = sanitized.match(/\.([^.]+)$/);
549
+ if (!match) {
550
+ return null;
551
+ }
552
+ const extension = match[1].toLowerCase();
553
+ if (!/^[a-z0-9]+$/.test(extension)) {
554
+ return null;
555
+ }
556
+ return extension;
557
+ },
558
+ /**
559
+ * Validate that an extension is a recognised image format.
560
+ * Case-insensitive; rejects extensions with special characters.
561
+ */
562
+ isValidImageExtension: (extension) => {
563
+ if (!extension || typeof extension !== "string") {
564
+ return false;
565
+ }
566
+ const normalizedExt = extension.toLowerCase();
567
+ if (!/^[a-z0-9]+$/.test(normalizedExt)) {
568
+ return false;
569
+ }
570
+ return VALID_IMAGE_EXTENSIONS_SET.has(normalizedExt);
571
+ },
572
+ /**
573
+ * Extract and validate image file extension from a filename or URL.
574
+ * Returns null if the extension is missing or not a recognised image format.
575
+ *
576
+ * Security note: the last extension is used, so "malware.exe.jpg" returns
577
+ * "jpg". Callers should apply additional checks (e.g. content inspection)
578
+ * where double-extension attacks are a concern.
579
+ */
580
+ getValidatedImageExtension: (filename) => {
581
+ const extension = imageUtils.getFileExtension(filename);
582
+ if (!extension) {
583
+ return null;
584
+ }
585
+ return imageUtils.isValidImageExtension(extension) ? extension : null;
474
586
  },
475
587
  /**
476
588
  * Convert file size to human readable format
@@ -576,7 +688,11 @@ export const imageUtils = {
576
688
  if (!/^image\//i.test(contentType)) {
577
689
  throw new Error(`Unsupported content-type: ${contentType || "unknown"}`);
578
690
  }
579
- const len = Number(response.headers.get("content-length") || 0);
691
+ const contentLengthHeader = response.headers.get("content-length");
692
+ const len = Number(contentLengthHeader || 0);
693
+ if (contentLengthHeader !== null && len === 0) {
694
+ throw new Error("Empty response: content-length is 0");
695
+ }
580
696
  if (len && len > maxBytes) {
581
697
  throw new Error(`Content too large: ${len} bytes`);
582
698
  }
@@ -1127,15 +1127,27 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1127
1127
  async function convertContentToProviderFormat(content, provider, _model) {
1128
1128
  const textContent = content.find((c) => c.type === "text");
1129
1129
  const imageContent = content.filter((c) => c.type === "image");
1130
- if (!textContent) {
1131
- throw new Error("Multimodal content must include at least one text element");
1132
- }
1133
- if (imageContent.length === 0) {
1134
- return textContent.text;
1130
+ const pdfContent = content.filter((c) => c.type === "pdf");
1131
+ // Allow empty text when multimodal content is present (enables image-only or PDF-only queries)
1132
+ const text = textContent?.text || "";
1133
+ const hasMultimodal = imageContent.length > 0 || pdfContent.length > 0;
1134
+ // Validate that we have at least some content
1135
+ if (!hasMultimodal && !text) {
1136
+ throw new Error("Content must include either text or multimodal content");
1137
+ }
1138
+ // Text-only case
1139
+ if (imageContent.length === 0 && pdfContent.length === 0) {
1140
+ return text;
1135
1141
  }
1136
1142
  // Extract images as Buffer | string array
1137
1143
  const images = imageContent.map((img) => img.data);
1138
- return await convertSimpleImagesToProviderFormat(textContent.text, images, provider, _model);
1144
+ // Extract PDFs in the expected format
1145
+ const pdfFiles = pdfContent.map((pdf) => ({
1146
+ buffer: typeof pdf.data === "string" ? Buffer.from(pdf.data, "base64") : pdf.data,
1147
+ filename: pdf.metadata?.filename || "document.pdf",
1148
+ pageCount: pdf.metadata?.pages ?? null,
1149
+ }));
1150
+ return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model);
1139
1151
  }
1140
1152
  /**
1141
1153
  * Check if a string is an internet URL
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.44.1",
3
+ "version": "9.48.3",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
6
6
  "author": {