@juspay/neurolink 9.95.2 → 10.0.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/browser/neurolink.min.js +380 -380
  3. package/dist/cli/commands/proxy.d.ts +26 -0
  4. package/dist/cli/commands/proxy.js +132 -13
  5. package/dist/cli/factories/commandFactory.d.ts +7 -0
  6. package/dist/cli/factories/commandFactory.js +29 -21
  7. package/dist/cli/utils/inputValidation.d.ts +1 -0
  8. package/dist/cli/utils/inputValidation.js +6 -1
  9. package/dist/core/constants.d.ts +3 -0
  10. package/dist/core/constants.js +10 -0
  11. package/dist/lib/core/constants.d.ts +3 -0
  12. package/dist/lib/core/constants.js +10 -0
  13. package/dist/lib/types/file.d.ts +38 -1
  14. package/dist/lib/utils/fileDetector.js +69 -6
  15. package/dist/lib/utils/imageCache.d.ts +10 -2
  16. package/dist/lib/utils/imageCache.js +12 -23
  17. package/dist/lib/utils/imageProcessor.d.ts +25 -0
  18. package/dist/lib/utils/imageProcessor.js +28 -1
  19. package/dist/lib/utils/logSanitize.d.ts +97 -0
  20. package/dist/lib/utils/logSanitize.js +163 -0
  21. package/dist/lib/utils/messageBuilder.js +36 -0
  22. package/dist/lib/utils/pdfProcessor.d.ts +30 -1
  23. package/dist/lib/utils/pdfProcessor.js +215 -52
  24. package/dist/types/file.d.ts +38 -1
  25. package/dist/utils/fileDetector.js +69 -6
  26. package/dist/utils/imageCache.d.ts +10 -2
  27. package/dist/utils/imageCache.js +12 -23
  28. package/dist/utils/imageProcessor.d.ts +25 -0
  29. package/dist/utils/imageProcessor.js +28 -1
  30. package/dist/utils/logSanitize.d.ts +97 -0
  31. package/dist/utils/logSanitize.js +163 -0
  32. package/dist/utils/messageBuilder.js +36 -0
  33. package/dist/utils/pdfProcessor.d.ts +30 -1
  34. package/dist/utils/pdfProcessor.js +215 -52
  35. package/package.json +1 -1
@@ -150,6 +150,13 @@ export class PDFProcessor {
150
150
  throw new Error(`PDF size ${sizeMB.toFixed(2)}MB exceeds ${config.maxSizeMB}MB limit for ${provider}`);
151
151
  }
152
152
  const metadata = PDFProcessor.extractBasicMetadata(content);
153
+ // #287: prefer an accurate page count (pdf-parse/pdfjs) over the unreliable
154
+ // header regex for both limit enforcement and returned metadata; fall back
155
+ // to the regex estimate when the accurate probe times out or fails.
156
+ const accuratePages = await PDFProcessor.getAccuratePageCount(content);
157
+ if (accuratePages !== null) {
158
+ metadata.estimatedPages = accuratePages;
159
+ }
153
160
  if (metadata.estimatedPages && metadata.estimatedPages > config.maxPages) {
154
161
  const enforceLimits = options?.enforceLimits !== false;
155
162
  if (enforceLimits) {
@@ -185,6 +192,10 @@ export class PDFProcessor {
185
192
  ...metadata,
186
193
  provider,
187
194
  apiType: config.apiType,
195
+ // #349: surface the provider's citations requirement so downstream
196
+ // provider adapters (e.g. Bedrock Converse document blocks) can act on
197
+ // it, instead of the config field being read nowhere.
198
+ requiresCitations: config.requiresCitations,
188
199
  },
189
200
  };
190
201
  }
@@ -220,6 +231,53 @@ export class PDFProcessor {
220
231
  filename: undefined,
221
232
  };
222
233
  }
234
+ /**
235
+ * Accurate page count via pdf-parse (pdfjs) (#287). The header regex in
236
+ * `extractBasicMetadata` only sees plaintext `/Type /Page` markers and misses
237
+ * compressed/object-stream PDFs (and miscounts when a page dict spans a chunk
238
+ * boundary). This parses the document properly, bounded by a timeout so a
239
+ * pathological PDF can't block; returns null on timeout/failure so the caller
240
+ * falls back to the regex estimate.
241
+ */
242
+ static async getAccuratePageCount(buffer) {
243
+ try {
244
+ const { PDFParse } = await import("pdf-parse");
245
+ const pdf = new PDFParse({ data: new Uint8Array(buffer) });
246
+ // Captured outside the try so the `finally` below can always clear it
247
+ // — otherwise a `getInfo()` that wins the race leaves this timer
248
+ // running until PAGE_COUNT_TIMEOUT_MS fires for nothing. `.unref()`
249
+ // so it can never itself hold the process open in the meantime.
250
+ let timeoutHandle;
251
+ try {
252
+ const info = await Promise.race([
253
+ pdf.getInfo(),
254
+ new Promise((_, reject) => {
255
+ timeoutHandle = setTimeout(() => reject(new Error("page-count timeout")), PDF_LIMITS.PAGE_COUNT_TIMEOUT_MS);
256
+ timeoutHandle.unref?.();
257
+ }),
258
+ ]);
259
+ const total = info.total;
260
+ return typeof total === "number" && total > 0 ? total : null;
261
+ }
262
+ finally {
263
+ if (timeoutHandle !== undefined) {
264
+ clearTimeout(timeoutHandle);
265
+ }
266
+ try {
267
+ await pdf.destroy?.();
268
+ }
269
+ catch {
270
+ // A throwing destroy() must not discard an otherwise-valid page
271
+ // count returned above (matches fileReferenceRegistry.ts's
272
+ // pdf.destroy() cleanup pattern) — swallow it.
273
+ }
274
+ }
275
+ }
276
+ catch (error) {
277
+ logger.debug(`[PDF] Accurate page count unavailable; using regex estimate: ${error instanceof Error ? error.message : String(error)}`);
278
+ return null;
279
+ }
280
+ }
223
281
  static estimateTokens(pageCount, mode = "visual") {
224
282
  if (mode === "text-only") {
225
283
  return Math.ceil((pageCount / 3) * 1000);
@@ -291,48 +349,32 @@ export class PDFProcessor {
291
349
  */
292
350
  static async convertToImages(pdfBuffer, options) {
293
351
  const startTime = Date.now();
294
- const { scale = 2, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", maxCanvasPixels = PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS, password, } = options || {};
352
+ const { scale = PDF_LIMITS.DEFAULT_SCALE, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", maxCanvasPixels = PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS, password, onProgress, } = options || {};
295
353
  const images = [];
296
354
  const warnings = [];
297
- // ============================================================================
298
- // INPUT VALIDATION (Security: Prevent malformed/malicious PDF processing)
299
- // ============================================================================
300
- // 0. Validate format is supported and case-sensitive
301
- if (format !== "png") {
302
- throw new Error(`Invalid format: "${format}". Only "png" format is currently supported.`);
303
- }
304
- // 0b. Validate the render scale. A non-finite or non-positive scale yields a
305
- // degenerate viewport (blank/zero-dimension render), and an excessive scale
306
- // can allocate hundreds of MB per page — reject both with a clear message.
307
- if (!Number.isFinite(scale) || scale <= 0 || scale > PDF_LIMITS.MAX_SCALE) {
308
- throw new Error(`Invalid scale: ${scale}. Scale must be a finite number greater than 0 and at most ${PDF_LIMITS.MAX_SCALE}.`);
309
- }
310
- // 0c. Validate the per-page pixel ceiling (#260). A non-finite or
311
- // non-positive value would disable the guard or yield a NaN downscale.
312
- if (!Number.isFinite(maxCanvasPixels) || maxCanvasPixels <= 0) {
313
- throw new Error(`Invalid maxCanvasPixels: ${maxCanvasPixels}. Must be a finite number greater than 0.`);
314
- }
315
- // 1. Validate buffer is not empty or too small
316
- if (!pdfBuffer || pdfBuffer.length < 5) {
317
- throw new Error("Invalid PDF: Buffer is too small or empty. " +
318
- "A valid PDF must be at least 5 bytes (PDF header).");
319
- }
320
- // 2. Validate PDF magic bytes (%PDF-)
321
- if (!PDFProcessor.isValidPDF(pdfBuffer)) {
322
- throw new Error("Invalid PDF: File must start with %PDF- header. " +
323
- "The provided buffer does not appear to be a valid PDF file.");
324
- }
325
- // 3. Validate maximum buffer size to prevent memory exhaustion
326
- const sizeMB = pdfBuffer.length / (1024 * 1024);
327
- if (sizeMB > PDF_LIMITS.MAX_SIZE_MB) {
328
- throw new Error(`PDF too large for image conversion: ${sizeMB.toFixed(2)}MB exceeds ${PDF_LIMITS.MAX_SIZE_MB}MB limit. ` +
329
- "Consider splitting the PDF or using a provider with native PDF support.");
330
- }
355
+ const pageErrors = [];
356
+ // Validation shared with convertToImagesStream (#302).
357
+ const sizeMB = PDFProcessor.validateImageConversionInput(pdfBuffer, {
358
+ format,
359
+ scale,
360
+ maxCanvasPixels,
361
+ });
331
362
  logger.debug("[PDF→Image] ✅ PDF validation passed", {
332
363
  bufferSize: pdfBuffer.length,
333
364
  sizeMB: sizeMB.toFixed(2),
334
365
  maxPages,
335
366
  });
367
+ // #297: surface an up-front memory estimate so an operator can spot a
368
+ // conversion that will allocate a lot before it runs. Uses the cheap regex
369
+ // page estimate (the accurate pdf-parse count runs in process(), not here).
370
+ const estimatedPages = Math.max(1, PDFProcessor.extractBasicMetadata(pdfBuffer).estimatedPages ?? 1);
371
+ const estimatedMemoryMB = PDFProcessor.estimateConversionMemoryUsage(pdfBuffer.length, estimatedPages, scale);
372
+ logger.info("[PDF→Image] Estimated memory usage before conversion", {
373
+ scale,
374
+ estimatedPages,
375
+ estimatedMemoryMB,
376
+ sizeMB: Number(sizeMB.toFixed(2)),
377
+ });
336
378
  try {
337
379
  // Dynamic import to avoid loading MuPDF binaries until needed
338
380
  const pdfToImgModule = await import("pdf-to-img");
@@ -369,35 +411,60 @@ export class PDFProcessor {
369
411
  logger.warn(`[PDF→Image] ⚠️ ${msg}`);
370
412
  warnings.push(msg);
371
413
  }
372
- // Create PDF document iterator (password forwarded for encrypted PDFs, #258)
414
+ // Create PDF document (password forwarded for encrypted PDFs, #258).
415
+ // pdf-to-img resolves `.length` (numPages) synchronously here and exposes
416
+ // `.getPage(n)`, so we drive an indexed loop rather than the async
417
+ // iterator — that lets one bad page be isolated instead of aborting all.
373
418
  const document = await pdf(pdfBuffer, {
374
419
  scale: effectiveScale,
375
420
  ...(password ? { password } : {}),
376
421
  });
377
- let pageIndex = 0;
378
- // Iterate through pages and convert to base64
379
- for await (const page of document) {
380
- // Check if we've reached the max pages limit
381
- if (maxPages !== undefined && pageIndex >= maxPages) {
382
- warnings.push(`Stopped at page ${pageIndex} (maxPages limit: ${maxPages})`);
422
+ const totalPages = document.length;
423
+ // #294: convert page-by-page with per-page isolation. A single page whose
424
+ // canvas render throws (e.g. a degenerate MediaBox) no longer discards
425
+ // every already-converted page it is recorded in `errors` and the rest
426
+ // continue.
427
+ for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
428
+ if (maxPages !== undefined && pageNum - 1 >= maxPages) {
429
+ warnings.push(`Stopped at page ${pageNum - 1} (maxPages limit: ${maxPages})`);
383
430
  break;
384
431
  }
385
- // Convert PNG buffer to base64
386
- const base64Image = page.toString("base64");
387
- images.push(base64Image);
388
- pageIndex++;
389
- logger.debug(`[PDF→Image] Converted page ${pageIndex}`, {
390
- imageSizeBytes: page.length,
391
- base64Length: base64Image.length,
392
- });
432
+ try {
433
+ const page = await document.getPage(pageNum);
434
+ const base64Image = page.toString("base64");
435
+ images.push(base64Image);
436
+ logger.debug(`[PDF→Image] Converted page ${pageNum}`, {
437
+ imageSizeBytes: page.length,
438
+ base64Length: base64Image.length,
439
+ });
440
+ // #302: report progress after each successfully converted page.
441
+ if (onProgress) {
442
+ await onProgress({
443
+ pagesConverted: images.length,
444
+ totalPages,
445
+ elapsedMs: Date.now() - startTime,
446
+ });
447
+ }
448
+ }
449
+ catch (pageError) {
450
+ const msg = pageError instanceof Error ? pageError.message : String(pageError);
451
+ logger.warn(`[PDF→Image] ⚠️ page ${pageNum} failed to render: ${msg}`);
452
+ pageErrors.push({ page: pageNum, error: msg });
453
+ }
393
454
  }
394
- // Check for empty PDF (0 pages)
455
+ // Empty PDF (0 pages) or every page failed → treat as a conversion
456
+ // failure (kept inside the try so the password/format mapping below still
457
+ // applies), otherwise return the pages that did convert.
395
458
  if (images.length === 0) {
459
+ if (pageErrors.length > 0) {
460
+ throw new Error(`All ${pageErrors.length} page(s) failed to render. First error: ${pageErrors[0].error}`);
461
+ }
396
462
  throw new Error("PDF has 0 pages. Cannot convert empty PDF to images.");
397
463
  }
398
464
  const conversionTimeMs = Date.now() - startTime;
399
465
  logger.info("[PDF→Image] ✅ PDF conversion completed", {
400
466
  pageCount: images.length,
467
+ failedPages: pageErrors.length,
401
468
  conversionTimeMs,
402
469
  totalImageBytes: images.reduce((sum, img) => sum + img.length, 0),
403
470
  });
@@ -406,6 +473,7 @@ export class PDFProcessor {
406
473
  pageCount: images.length,
407
474
  conversionTimeMs,
408
475
  warnings: warnings.length > 0 ? warnings : undefined,
476
+ errors: pageErrors.length > 0 ? pageErrors : undefined,
409
477
  };
410
478
  }
411
479
  catch (error) {
@@ -431,6 +499,101 @@ export class PDFProcessor {
431
499
  });
432
500
  }
433
501
  }
502
+ /**
503
+ * Shared input validation for the image-conversion paths. Returns the PDF
504
+ * size in MB (needed by the memory-estimate log). Throws on any invalid input
505
+ * with the same messages the batch path has always used.
506
+ */
507
+ static validateImageConversionInput(pdfBuffer, opts) {
508
+ if (opts.format !== "png") {
509
+ throw new Error(`Invalid format: "${opts.format}". Only "png" format is currently supported.`);
510
+ }
511
+ if (!Number.isFinite(opts.scale) ||
512
+ opts.scale < PDF_LIMITS.MIN_SCALE ||
513
+ opts.scale > PDF_LIMITS.MAX_SCALE) {
514
+ throw new Error(`Invalid scale: ${opts.scale}. Scale must be a finite number between ${PDF_LIMITS.MIN_SCALE} and ${PDF_LIMITS.MAX_SCALE}.`);
515
+ }
516
+ if (!Number.isFinite(opts.maxCanvasPixels) || opts.maxCanvasPixels <= 0) {
517
+ throw new Error(`Invalid maxCanvasPixels: ${opts.maxCanvasPixels}. Must be a finite number greater than 0.`);
518
+ }
519
+ if (!pdfBuffer || pdfBuffer.length < 5) {
520
+ throw new Error("Invalid PDF: Buffer is too small or empty. " +
521
+ "A valid PDF must be at least 5 bytes (PDF header).");
522
+ }
523
+ if (!PDFProcessor.isValidPDF(pdfBuffer)) {
524
+ throw new Error("Invalid PDF: File must start with %PDF- header. " +
525
+ "The provided buffer does not appear to be a valid PDF file.");
526
+ }
527
+ const sizeMB = pdfBuffer.length / (1024 * 1024);
528
+ if (sizeMB > PDF_LIMITS.MAX_SIZE_MB) {
529
+ throw new Error(`PDF too large for image conversion: ${sizeMB.toFixed(2)}MB exceeds ${PDF_LIMITS.MAX_SIZE_MB}MB limit. ` +
530
+ "Consider splitting the PDF or using a provider with native PDF support.");
531
+ }
532
+ return sizeMB;
533
+ }
534
+ /**
535
+ * Streaming variant of {@link convertToImages} (#302): yields each page's
536
+ * base64 PNG as soon as it renders instead of buffering the whole document,
537
+ * and reports progress via `options.onProgress`. A page that fails to render
538
+ * is yielded with `error` set (per-page isolation, #294) rather than aborting
539
+ * the stream.
540
+ *
541
+ * NOT a wrapper of/over {@link convertToImages}, despite the similar
542
+ * contract — the two are independent, parallel implementations (each does
543
+ * its own `pdf-to-img` import, downscale calculation, and page loop)
544
+ * rather than one delegating to the other. Keep behavior changes (page
545
+ * isolation, downscale, password handling) in sync across both by hand.
546
+ */
547
+ static async *convertToImagesStream(pdfBuffer, options) {
548
+ const startTime = Date.now();
549
+ const { scale = PDF_LIMITS.DEFAULT_SCALE, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", maxCanvasPixels = PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS, password, onProgress, } = options || {};
550
+ PDFProcessor.validateImageConversionInput(pdfBuffer, {
551
+ format,
552
+ scale,
553
+ maxCanvasPixels,
554
+ });
555
+ const pdfToImgModule = await import("pdf-to-img");
556
+ const pdf = pdfToImgModule.pdf;
557
+ // #260: uniform downscale so the largest page stays under maxCanvasPixels.
558
+ let effectiveScale = scale;
559
+ const largestPixels = PDFProcessor.largestPagePixels(pdfBuffer, scale);
560
+ if (largestPixels > maxCanvasPixels) {
561
+ effectiveScale = scale * Math.sqrt(maxCanvasPixels / largestPixels);
562
+ }
563
+ const document = await pdf(pdfBuffer, {
564
+ scale: effectiveScale,
565
+ ...(password ? { password } : {}),
566
+ });
567
+ const totalPages = document.length;
568
+ let converted = 0;
569
+ for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
570
+ if (maxPages !== undefined && pageNum - 1 >= maxPages) {
571
+ break;
572
+ }
573
+ try {
574
+ const page = await document.getPage(pageNum);
575
+ const base64Image = page.toString("base64");
576
+ converted++;
577
+ if (onProgress) {
578
+ await onProgress({
579
+ pagesConverted: converted,
580
+ totalPages,
581
+ elapsedMs: Date.now() - startTime,
582
+ });
583
+ }
584
+ yield {
585
+ pageIndex: pageNum,
586
+ image: base64Image,
587
+ imageSizeBytes: page.length,
588
+ };
589
+ }
590
+ catch (pageError) {
591
+ const msg = pageError instanceof Error ? pageError.message : String(pageError);
592
+ logger.warn(`[PDF→Image] ⚠️ page ${pageNum} failed to render (stream): ${msg}`);
593
+ yield { pageIndex: pageNum, image: "", imageSizeBytes: 0, error: msg };
594
+ }
595
+ }
596
+ }
434
597
  /**
435
598
  * Convert a PDF file path to an array of base64 PNG images
436
599
  *
@@ -466,7 +629,7 @@ export class PDFProcessor {
466
629
  * @param scale - Scale factor
467
630
  * @returns Estimated memory usage in MB
468
631
  */
469
- static estimateConversionMemoryUsage(pdfSizeBytes, pageCount, scale = 2) {
632
+ static estimateConversionMemoryUsage(pdfSizeBytes, pageCount, scale = PDF_LIMITS.DEFAULT_SCALE) {
470
633
  // Rough estimation:
471
634
  // - Each page at scale 2 produces ~1-3MB PNG
472
635
  // - MuPDF needs ~2x PDF size for processing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.95.2",
3
+ "version": "10.0.0",
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": {