@juspay/neurolink 8.6.0 → 8.8.0

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.
@@ -3,6 +3,57 @@
3
3
  * Handles format conversion for different AI providers
4
4
  */
5
5
  import { logger } from "./logger.js";
6
+ import { withRetry } from "./retryHandler.js";
7
+ import { SYSTEM_LIMITS } from "../core/constants.js";
8
+ /**
9
+ * Network error codes that should trigger a retry
10
+ */
11
+ const RETRYABLE_ERROR_CODES = new Set([
12
+ "ECONNRESET",
13
+ "ENOTFOUND",
14
+ "ECONNREFUSED",
15
+ "ETIMEDOUT",
16
+ "ERR_NETWORK",
17
+ ]);
18
+ /**
19
+ * Determines if an HTTP error is retryable based on status code
20
+ * Only network errors and certain HTTP status codes should be retried
21
+ * 4xx client errors like 404 (Not Found) and 403 (Forbidden) should NOT be retried
22
+ *
23
+ * @param error - The error to check
24
+ * @returns true if the error is retryable, false otherwise
25
+ */
26
+ function isRetryableDownloadError(error) {
27
+ // Network-related errors should be retried
28
+ if (error && typeof error === "object") {
29
+ const errorCode = error.code;
30
+ const errorName = error.name;
31
+ if (RETRYABLE_ERROR_CODES.has(errorCode || "") ||
32
+ errorName === "AbortError") {
33
+ return true;
34
+ }
35
+ }
36
+ // Check for HTTP status code in error message for retryable errors
37
+ // Only retry on 5xx server errors, 429 (Too Many Requests), and 408 (Request Timeout)
38
+ // Do NOT retry on 4xx client errors like 404 (Not Found) or 403 (Forbidden)
39
+ if (error instanceof Error) {
40
+ const message = error.message;
41
+ // Extract HTTP status from error message like "HTTP 503: Service Unavailable"
42
+ const statusMatch = message.match(/HTTP (\d{3}):/);
43
+ if (statusMatch) {
44
+ const status = parseInt(statusMatch[1], 10);
45
+ // Retry on 5xx server errors, 429 (rate limit), 408 (timeout)
46
+ return status >= 500 || status === 429 || status === 408;
47
+ }
48
+ // Check for timeout/network-related error messages
49
+ // Use more precise matching to avoid false positives like "No timeout specified"
50
+ if (/\b(request timed out|operation timed out|connection timed out|timed out)\b/i.test(message) ||
51
+ /\bnetwork (error|failure|unreachable|down)\b/i.test(message)) {
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ }
6
57
  /**
7
58
  * Image processor class for handling provider-specific image formatting
8
59
  */
@@ -16,9 +67,16 @@ export class ImageProcessor {
16
67
  * @returns Processed image as data URI
17
68
  */
18
69
  static async process(content, _options) {
70
+ // Validate content is non-empty before processing
71
+ if (content.length === 0) {
72
+ logger.error("Empty buffer provided");
73
+ throw new Error("Invalid image processing: buffer is empty");
74
+ }
19
75
  const mediaType = this.detectImageType(content);
20
76
  const base64 = content.toString("base64");
21
77
  const dataUri = `data:${mediaType};base64,${base64}`;
78
+ // Validate output before returning
79
+ this.validateProcessOutput(dataUri, base64, mediaType);
22
80
  return {
23
81
  type: "image",
24
82
  content: dataUri,
@@ -29,6 +87,37 @@ export class ImageProcessor {
29
87
  },
30
88
  };
31
89
  }
90
+ /**
91
+ * Validate processed output meets required format
92
+ * Checks:
93
+ * - Base64 content is non-empty
94
+ * - Data URI format is valid (data:{mimeType};base64,{content})
95
+ * - MIME type is in the allowed list
96
+ * @param dataUri - The complete data URI string
97
+ * @param base64 - The base64-encoded content
98
+ * @param mediaType - The MIME type of the image
99
+ * @throws Error if any validation fails
100
+ */
101
+ static validateProcessOutput(dataUri, base64, mediaType) {
102
+ // Validate base64 is non-empty (check first for better error message)
103
+ if (base64.length === 0) {
104
+ logger.error("Empty base64 content generated");
105
+ throw new Error("Invalid image processing: base64 content is empty");
106
+ }
107
+ // Validate data URI format with proper base64 character validation
108
+ // Base64 can only have 0, 1, or 2 padding characters at the end
109
+ const dataUriRegex = /^data:[^;]+;base64,[A-Za-z0-9+/]*={0,2}$/;
110
+ if (!dataUriRegex.test(dataUri)) {
111
+ logger.error("Invalid data URI format generated", { dataUri });
112
+ throw new Error("Invalid data URI format: must be data:{mimeType};base64,{content}");
113
+ }
114
+ // Defensive check: ensure detectImageType() returns valid MIME type
115
+ // This validation protects against future changes to detectImageType()
116
+ if (!this.validateImageFormat(mediaType)) {
117
+ logger.error("Invalid MIME type generated", { mediaType });
118
+ throw new Error(`Invalid MIME type: ${mediaType} is not in allowed list`);
119
+ }
120
+ }
32
121
  /**
33
122
  * Process image for OpenAI (requires data URI format)
34
123
  */
@@ -434,14 +523,35 @@ export const imageUtils = {
434
523
  }
435
524
  },
436
525
  /**
437
- * Convert URL to base64 data URI by downloading the image
526
+ * Convert URL to base64 data URI by downloading the image.
527
+ * Implements retry logic with exponential backoff for network errors.
528
+ *
529
+ * Retries are performed for:
530
+ * - Network errors (ECONNRESET, ENOTFOUND, ECONNREFUSED, ETIMEDOUT, ERR_NETWORK, AbortError)
531
+ * - Server errors (5xx status codes)
532
+ * - Rate limiting (429 Too Many Requests)
533
+ * - Request timeouts (408 Request Timeout)
534
+ *
535
+ * Retries are NOT performed for:
536
+ * - Client errors (4xx status codes except 408, 429)
537
+ * - Invalid content type
538
+ * - Content size limit exceeded
539
+ * - Unsupported protocol
540
+ *
541
+ * @param url - The URL of the image to download
542
+ * @param options - Configuration options
543
+ * @param options.timeoutMs - Timeout for each download attempt (default: 15000ms)
544
+ * @param options.maxBytes - Maximum allowed file size (default: 10MB)
545
+ * @param options.maxAttempts - Maximum number of total attempts including initial attempt (default: 3)
546
+ * @returns Promise<string> - Base64 data URI of the downloaded image
438
547
  */
439
- urlToBase64DataUri: async (url, { timeoutMs = 15000, maxBytes = 10 * 1024 * 1024 } = {}) => {
440
- try {
441
- // Basic protocol whitelist
442
- if (!/^https?:\/\//i.test(url)) {
443
- throw new Error("Unsupported protocol");
444
- }
548
+ urlToBase64DataUri: async (url, { timeoutMs = 15000, maxBytes = 10 * 1024 * 1024, maxAttempts = 3, } = {}) => {
549
+ // Basic protocol whitelist - fail fast, no retry needed
550
+ if (!/^https?:\/\//i.test(url)) {
551
+ throw new Error("Unsupported protocol");
552
+ }
553
+ // Perform the actual download with retry logic
554
+ const performDownload = async () => {
445
555
  const controller = new AbortController();
446
556
  const t = setTimeout(() => controller.abort(), timeoutMs);
447
557
  try {
@@ -467,6 +577,20 @@ export const imageUtils = {
467
577
  finally {
468
578
  clearTimeout(t);
469
579
  }
580
+ };
581
+ try {
582
+ return await withRetry(performDownload, {
583
+ maxAttempts,
584
+ initialDelay: SYSTEM_LIMITS.DEFAULT_INITIAL_DELAY,
585
+ backoffMultiplier: SYSTEM_LIMITS.DEFAULT_BACKOFF_MULTIPLIER,
586
+ maxDelay: SYSTEM_LIMITS.DEFAULT_MAX_DELAY,
587
+ retryCondition: isRetryableDownloadError,
588
+ onRetry: (attempt, error) => {
589
+ const message = error instanceof Error ? error.message : String(error);
590
+ const attemptsLeft = maxAttempts - attempt;
591
+ logger.warn(`⚠️ Image download attempt ${attempt} failed for ${url}: ${message}. ${attemptsLeft} ${attemptsLeft === 1 ? "attempt" : "attempts"} remaining...`);
592
+ },
593
+ });
470
594
  }
471
595
  catch (error) {
472
596
  throw new Error(`Failed to download and convert URL to base64: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1,6 +1,6 @@
1
1
  import { logger } from "./logger.js";
2
- import * as pdfjs from "pdfjs-dist/legacy/build/pdf.mjs";
3
- import { createCanvas } from "canvas";
2
+ // Lazy-load pdfjs-dist to avoid DOMMatrix errors in Node.js server environment
3
+ // import * as pdfjs from "pdfjs-dist/legacy/build/pdf.mjs";
4
4
  const PDF_PROVIDER_CONFIGS = {
5
5
  anthropic: {
6
6
  maxSizeMB: 5,
@@ -196,6 +196,28 @@ export class PDFProcessor {
196
196
  }
197
197
  }
198
198
  static async convertPDFToImages(pdfBuffer, options) {
199
+ // Dynamic import canvas - only load when actually needed
200
+ let createCanvas;
201
+ try {
202
+ const canvasModule = await import("canvas");
203
+ createCanvas = canvasModule.createCanvas;
204
+ }
205
+ catch {
206
+ throw new Error("Canvas dependency not available. " +
207
+ "PDF-to-image conversion requires the 'canvas' package with native bindings. " +
208
+ "Install with: pnpm install canvas\n" +
209
+ "Note: This requires native build tools (Python, C++ compiler).");
210
+ }
211
+ // Dynamic import pdfjs - only load when actually needed to avoid DOMMatrix errors
212
+ let pdfjs;
213
+ try {
214
+ pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
215
+ }
216
+ catch {
217
+ throw new Error("pdfjs-dist dependency not available. " +
218
+ "PDF processing requires the 'pdfjs-dist' package. " +
219
+ "Install with: pnpm install pdfjs-dist");
220
+ }
199
221
  const maxPages = options?.maxPages || 10;
200
222
  const scale = options?.scale || 2.0;
201
223
  const format = options?.format || "png";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "8.6.0",
3
+ "version": "8.8.0",
4
4
  "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 9 major providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
5
5
  "author": {
6
6
  "name": "Juspay Technologies",
@@ -179,7 +179,6 @@
179
179
  "@opentelemetry/sdk-trace-node": "^2.1.0",
180
180
  "@opentelemetry/semantic-conventions": "^1.30.1",
181
181
  "ai": "4.3.16",
182
- "canvas": "^3.2.0",
183
182
  "chalk": "^5.6.2",
184
183
  "csv-parser": "^3.2.0",
185
184
  "dotenv": "^16.6.1",
@@ -201,6 +200,9 @@
201
200
  "zod": "^3.22.0",
202
201
  "zod-to-json-schema": "^3.24.6"
203
202
  },
203
+ "optionalDependencies": {
204
+ "canvas": "^3.2.0"
205
+ },
204
206
  "devDependencies": {
205
207
  "@biomejs/biome": "^2.2.4",
206
208
  "@changesets/changelog-github": "^0.5.1",
@@ -210,7 +212,7 @@
210
212
  "@semantic-release/commit-analyzer": "^13.0.1",
211
213
  "@semantic-release/git": "^10.0.1",
212
214
  "@semantic-release/github": "^11.0.6",
213
- "@semantic-release/npm": "^12.0.2",
215
+ "@semantic-release/npm": "^13.1.2",
214
216
  "@semantic-release/release-notes-generator": "^14.1.0",
215
217
  "@smithy/types": "^4.5.0",
216
218
  "@sveltejs/adapter-auto": "^6.1.0",
@@ -297,7 +299,8 @@
297
299
  "@eslint/plugin-kit@<0.3.4": ">=0.3.4",
298
300
  "tmp@<=0.2.3": ">=0.2.4",
299
301
  "axios@<1.8.2": ">=1.8.2",
300
- "glob@>=10.3.7 <=11.0.3": ">=11.1.0"
302
+ "glob@>=10.3.7 <=11.0.3": ">=11.1.0",
303
+ "@semantic-release/npm": "^13.1.2"
301
304
  }
302
305
  },
303
306
  "os": [