@juspay/neurolink 9.95.1 → 9.95.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/neurolink.min.js +176 -176
  3. package/dist/cli/factories/commandFactory.d.ts +10 -0
  4. package/dist/cli/factories/commandFactory.js +30 -0
  5. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  6. package/dist/core/baseProvider.js +1 -0
  7. package/dist/core/constants.d.ts +2 -0
  8. package/dist/core/constants.js +9 -0
  9. package/dist/core/modules/MessageBuilder.js +2 -0
  10. package/dist/lib/core/baseProvider.js +1 -0
  11. package/dist/lib/core/constants.d.ts +2 -0
  12. package/dist/lib/core/constants.js +9 -0
  13. package/dist/lib/core/modules/MessageBuilder.js +2 -0
  14. package/dist/lib/neurolink.js +8 -4
  15. package/dist/lib/types/file.d.ts +10 -0
  16. package/dist/lib/types/generate.d.ts +14 -0
  17. package/dist/lib/types/stream.d.ts +7 -0
  18. package/dist/lib/utils/errorHandling.d.ts +10 -0
  19. package/dist/lib/utils/errorHandling.js +28 -0
  20. package/dist/lib/utils/messageBuilder.js +36 -5
  21. package/dist/lib/utils/multimodalOptionsBuilder.d.ts +4 -0
  22. package/dist/lib/utils/multimodalOptionsBuilder.js +1 -0
  23. package/dist/lib/utils/pdfProcessor.d.ts +15 -0
  24. package/dist/lib/utils/pdfProcessor.js +83 -3
  25. package/dist/neurolink.js +8 -4
  26. package/dist/types/file.d.ts +10 -0
  27. package/dist/types/generate.d.ts +14 -0
  28. package/dist/types/stream.d.ts +7 -0
  29. package/dist/utils/errorHandling.d.ts +10 -0
  30. package/dist/utils/errorHandling.js +28 -0
  31. package/dist/utils/messageBuilder.js +36 -5
  32. package/dist/utils/multimodalOptionsBuilder.d.ts +4 -0
  33. package/dist/utils/multimodalOptionsBuilder.js +1 -0
  34. package/dist/utils/pdfProcessor.d.ts +15 -0
  35. package/dist/utils/pdfProcessor.js +83 -3
  36. package/package.json +1 -1
@@ -22,6 +22,16 @@ export declare class CLICommandFactory {
22
22
  private static processCliImages;
23
23
  private static processCliCSVFiles;
24
24
  private static processCliPDFFiles;
25
+ /**
26
+ * Resolve the PDF decryption password, preferring the NEUROLINK_PDF_PASSWORD
27
+ * env var over the `--pdf-password` flag. A plaintext CLI flag leaks into
28
+ * shell history, `ps`/process listings, and CI logs — the env var avoids
29
+ * that, matching the project's existing "credentials via env vars, not
30
+ * flags" stance for the loop session. The flag stays supported (dropping it
31
+ * would be a breaking CLI change), but using it prints a one-line stderr
32
+ * warning recommending the env var instead.
33
+ */
34
+ private static resolvePdfPassword;
25
35
  private static processCliFiles;
26
36
  private static processCliVideoFiles;
27
37
  private static processOptions;
@@ -156,6 +156,12 @@ export class CLICommandFactory {
156
156
  type: "string",
157
157
  description: "Add PDF file for analysis (can be used multiple times)",
158
158
  },
159
+ "pdf-password": {
160
+ type: "string",
161
+ description: "Password for an encrypted PDF (used on the image-conversion path). " +
162
+ "Visible in shell history/process listings — prefer the " +
163
+ "NEUROLINK_PDF_PASSWORD env var instead.",
164
+ },
159
165
  video: {
160
166
  type: "string",
161
167
  description: "Add video file for analysis (can be used multiple times) (MP4, WebM, MOV, AVI, MKV)",
@@ -793,6 +799,24 @@ export class CLICommandFactory {
793
799
  // URLs are preserved as-is by resolveFilePaths
794
800
  return resolveFilePaths(paths);
795
801
  }
802
+ /**
803
+ * Resolve the PDF decryption password, preferring the NEUROLINK_PDF_PASSWORD
804
+ * env var over the `--pdf-password` flag. A plaintext CLI flag leaks into
805
+ * shell history, `ps`/process listings, and CI logs — the env var avoids
806
+ * that, matching the project's existing "credentials via env vars, not
807
+ * flags" stance for the loop session. The flag stays supported (dropping it
808
+ * would be a breaking CLI change), but using it prints a one-line stderr
809
+ * warning recommending the env var instead.
810
+ */
811
+ static resolvePdfPassword(argv) {
812
+ const flagValue = argv.pdfPassword;
813
+ const envValue = process.env.NEUROLINK_PDF_PASSWORD;
814
+ if (flagValue) {
815
+ process.stderr.write(chalk.yellow("⚠️ --pdf-password is visible in shell history and process listings. " +
816
+ "Prefer the NEUROLINK_PDF_PASSWORD environment variable instead.\n"));
817
+ }
818
+ return flagValue || envValue;
819
+ }
796
820
  // Helper method to process CLI files with auto-detection
797
821
  static processCliFiles(files) {
798
822
  if (!files) {
@@ -2649,6 +2673,9 @@ export class CLICommandFactory {
2649
2673
  const inputAudioFormat = inferAudioFormatFromPath(inputAudioPath);
2650
2674
  const runGenerate = () => sdk.generate({
2651
2675
  input: generateInput,
2676
+ pdfOptions: {
2677
+ password: CLICommandFactory.resolvePdfPassword(argv),
2678
+ },
2652
2679
  csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
2653
2680
  videoOptions: {
2654
2681
  frames: argv.videoFrames,
@@ -2901,6 +2928,9 @@ export class CLICommandFactory {
2901
2928
  ...(videoFiles && { videoFiles }),
2902
2929
  ...(files && { files }),
2903
2930
  },
2931
+ pdfOptions: {
2932
+ password: CLICommandFactory.resolvePdfPassword(argv),
2933
+ },
2904
2934
  csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
2905
2935
  videoOptions: {
2906
2936
  frames: argv.videoFrames,
@@ -4,4 +4,4 @@ import type { OptionSchema, TextGenerationOptions } from "../../lib/types/index.
4
4
  * This object provides metadata for validation and help text in the CLI loop.
5
5
  * It is derived from the main TextGenerationOptions interface to ensure consistency.
6
6
  */
7
- export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation">, OptionSchema>;
7
+ export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation">, OptionSchema>;
@@ -420,6 +420,7 @@ export class BaseProvider {
420
420
  toolUsageContext: options.toolUsageContext,
421
421
  context: options.context,
422
422
  csvOptions: options.csvOptions,
423
+ pdfOptions: options.pdfOptions,
423
424
  // Forward abort, tool filtering, and timeout options to prevent
424
425
  // silent bypass when falling back from real streaming to fake streaming
425
426
  abortSignal: options.abortSignal,
@@ -122,6 +122,8 @@ export declare const PDF_LIMITS: {
122
122
  MAX_SIZE_MB: number;
123
123
  DEFAULT_MAX_PAGES: number;
124
124
  MAX_SCALE: number;
125
+ DEFAULT_MAX_CANVAS_PIXELS: number;
126
+ MIN_EFFECTIVE_SCALE: number;
125
127
  };
126
128
  export declare const SYSTEM_LIMITS: {
127
129
  MAX_PROMPT_LENGTH: number;
@@ -220,6 +220,15 @@ export const PDF_LIMITS = {
220
220
  // Upper bound for the render scale factor. Above this, a single page can
221
221
  // allocate hundreds of MB; scale <= 0 produces a degenerate viewport.
222
222
  MAX_SCALE: 10,
223
+ // Default per-page pixel ceiling for image conversion (#260). 16.7M px ×
224
+ // 4 bytes RGBA ≈ 64 MB per page — safely bounded. A larger page is
225
+ // uniformly downscaled to stay under this instead of allocating gigabytes.
226
+ DEFAULT_MAX_CANVAS_PIXELS: 16_777_216,
227
+ // Floor for the downscaled render scale (#260 follow-up). A crafted/malformed
228
+ // MediaBox can drive the estimated pixel count toward Infinity, which would
229
+ // otherwise collapse `effectiveScale` to 0 and hand `pdf-to-img` a degenerate
230
+ // viewport. Never let the downscale branch go below this.
231
+ MIN_EFFECTIVE_SCALE: 0.1,
223
232
  };
224
233
  // Performance and System Limits
225
234
  export const SYSTEM_LIMITS = {
@@ -103,6 +103,7 @@ export class MessageBuilder {
103
103
  files: input?.files,
104
104
  },
105
105
  csvOptions: options.csvOptions,
106
+ pdfOptions: options.pdfOptions,
106
107
  provider: options.provider,
107
108
  model: options.model,
108
109
  temperature: options.temperature,
@@ -224,6 +225,7 @@ export class MessageBuilder {
224
225
  files: input?.files,
225
226
  },
226
227
  csvOptions: options.csvOptions,
228
+ pdfOptions: options.pdfOptions,
227
229
  provider: options.provider,
228
230
  model: options.model,
229
231
  temperature: options.temperature,
@@ -420,6 +420,7 @@ export class BaseProvider {
420
420
  toolUsageContext: options.toolUsageContext,
421
421
  context: options.context,
422
422
  csvOptions: options.csvOptions,
423
+ pdfOptions: options.pdfOptions,
423
424
  // Forward abort, tool filtering, and timeout options to prevent
424
425
  // silent bypass when falling back from real streaming to fake streaming
425
426
  abortSignal: options.abortSignal,
@@ -122,6 +122,8 @@ export declare const PDF_LIMITS: {
122
122
  MAX_SIZE_MB: number;
123
123
  DEFAULT_MAX_PAGES: number;
124
124
  MAX_SCALE: number;
125
+ DEFAULT_MAX_CANVAS_PIXELS: number;
126
+ MIN_EFFECTIVE_SCALE: number;
125
127
  };
126
128
  export declare const SYSTEM_LIMITS: {
127
129
  MAX_PROMPT_LENGTH: number;
@@ -220,6 +220,15 @@ export const PDF_LIMITS = {
220
220
  // Upper bound for the render scale factor. Above this, a single page can
221
221
  // allocate hundreds of MB; scale <= 0 produces a degenerate viewport.
222
222
  MAX_SCALE: 10,
223
+ // Default per-page pixel ceiling for image conversion (#260). 16.7M px ×
224
+ // 4 bytes RGBA ≈ 64 MB per page — safely bounded. A larger page is
225
+ // uniformly downscaled to stay under this instead of allocating gigabytes.
226
+ DEFAULT_MAX_CANVAS_PIXELS: 16_777_216,
227
+ // Floor for the downscaled render scale (#260 follow-up). A crafted/malformed
228
+ // MediaBox can drive the estimated pixel count toward Infinity, which would
229
+ // otherwise collapse `effectiveScale` to 0 and hand `pdf-to-img` a degenerate
230
+ // viewport. Never let the downscale branch go below this.
231
+ MIN_EFFECTIVE_SCALE: 0.1,
223
232
  };
224
233
  // Performance and System Limits
225
234
  export const SYSTEM_LIMITS = {
@@ -103,6 +103,7 @@ export class MessageBuilder {
103
103
  files: input?.files,
104
104
  },
105
105
  csvOptions: options.csvOptions,
106
+ pdfOptions: options.pdfOptions,
106
107
  provider: options.provider,
107
108
  model: options.model,
108
109
  temperature: options.temperature,
@@ -224,6 +225,7 @@ export class MessageBuilder {
224
225
  files: input?.files,
225
226
  },
226
227
  csvOptions: options.csvOptions,
228
+ pdfOptions: options.pdfOptions,
227
229
  provider: options.provider,
228
230
  model: options.model,
229
231
  temperature: options.temperature,
@@ -3899,11 +3899,15 @@ Current user's request: ${currentInput}`;
3899
3899
  evaluationDomain: options.evaluationDomain,
3900
3900
  toolUsageContext: options.toolUsageContext,
3901
3901
  input: options.input,
3902
- // CSV processing options must survive the reconstruction into
3903
- // TextGenerationOptions — the message builder reads them (encoding,
3904
- // formatting, sanitization, parse timeout). Omitting them silently
3905
- // dropped every csvOptions field (incl. the CLI --csv-* flags).
3902
+ // Multimodal file-processing options must survive the reconstruction
3903
+ // into TextGenerationOptions — the message builder reads them downstream
3904
+ // (csv encoding/formatting/sanitization/parse timeout, PDF
3905
+ // password/decryption #258). Omitting them here silently dropped e.g.
3906
+ // every csvOptions field (incl. the CLI --csv-* flags) or
3907
+ // pdfOptions.password so the CLI --pdf-password / SDK pdfOptions never
3908
+ // reached convertToImages.
3906
3909
  csvOptions: options.csvOptions,
3910
+ pdfOptions: options.pdfOptions,
3907
3911
  region: options.region,
3908
3912
  tts: options.tts,
3909
3913
  stt: options.stt,
@@ -226,6 +226,8 @@ export type PDFProcessorOptions = {
226
226
  * Set to false to bypass limit enforcement (logs warning instead)
227
227
  */
228
228
  enforceLimits?: boolean;
229
+ /** Password for an encrypted PDF (used on the image-conversion path) (#258). */
230
+ password?: string;
229
231
  };
230
232
  /**
231
233
  * Audio provider configuration for transcription services
@@ -395,6 +397,14 @@ export type PDFImageConversionOptions = {
395
397
  maxPages?: number;
396
398
  /** Output format (default: png). Only PNG is currently implemented by PDFProcessor. */
397
399
  format?: "png";
400
+ /**
401
+ * Per-page pixel ceiling (#260). Any page whose width×height×scale² would
402
+ * exceed this is uniformly downscaled to stay under it, preventing a huge
403
+ * page from allocating gigabytes of canvas. Default: PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS.
404
+ */
405
+ maxCanvasPixels?: number;
406
+ /** Password for an encrypted PDF (passed to the underlying renderer) (#258). */
407
+ password?: string;
398
408
  };
399
409
  /** Result of PDF to image conversion. */
400
410
  export type PDFImageConversionResult = {
@@ -125,6 +125,13 @@ export type GenerateOptions = {
125
125
  music?: MusicOptions;
126
126
  };
127
127
  csvOptions?: CSVProcessorOptions;
128
+ /** PDF processing options (#258). */
129
+ pdfOptions?: {
130
+ /** Password for an encrypted PDF (image-conversion fallback path). */
131
+ password?: string;
132
+ /** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
133
+ maxCanvasPixels?: number;
134
+ };
128
135
  videoOptions?: {
129
136
  frames?: number;
130
137
  quality?: number;
@@ -1131,6 +1138,13 @@ export type TextGenerationOptions = {
1131
1138
  expectedOutcome?: string;
1132
1139
  evaluationCriteria?: string[];
1133
1140
  csvOptions?: CSVProcessorOptions;
1141
+ /** PDF processing options (#258). */
1142
+ pdfOptions?: {
1143
+ /** Password for an encrypted PDF (image-conversion fallback path). */
1144
+ password?: string;
1145
+ /** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
1146
+ maxCanvasPixels?: number;
1147
+ };
1134
1148
  enableSummarization?: boolean;
1135
1149
  /**
1136
1150
  * Skip injecting tool schemas into the system prompt.
@@ -207,6 +207,13 @@ export type StreamOptions = {
207
207
  };
208
208
  };
209
209
  csvOptions?: CSVProcessorOptions;
210
+ /** PDF processing options (#258). */
211
+ pdfOptions?: {
212
+ /** Password for an encrypted PDF (image-conversion fallback path). */
213
+ password?: string;
214
+ /** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
215
+ maxCanvasPixels?: number;
216
+ };
210
217
  videoOptions?: {
211
218
  frames?: number;
212
219
  quality?: number;
@@ -39,6 +39,8 @@ export declare const ERROR_CODES: {
39
39
  readonly FILE_PROCESSING_FAILED: "FILE_PROCESSING_FAILED";
40
40
  readonly CSV_PROCESSING_FAILED: "CSV_PROCESSING_FAILED";
41
41
  readonly PDF_PAGE_LIMIT_EXCEEDED: "PDF_PAGE_LIMIT_EXCEEDED";
42
+ readonly PDF_PASSWORD_REQUIRED: "PDF_PASSWORD_REQUIRED";
43
+ readonly PDF_INCORRECT_PASSWORD: "PDF_INCORRECT_PASSWORD";
42
44
  readonly RATE_LIMITER_QUEUE_FULL: "RATE_LIMITER_QUEUE_FULL";
43
45
  readonly RATE_LIMITER_QUEUE_TIMEOUT: "RATE_LIMITER_QUEUE_TIMEOUT";
44
46
  readonly RATE_LIMITER_RESET: "RATE_LIMITER_RESET";
@@ -199,6 +201,14 @@ export declare class ErrorFactory {
199
201
  * Create a PDF page limit exceeded error
200
202
  */
201
203
  static pdfPageLimitExceeded(estimatedPages: number, maxPages: number, provider: string): NeuroLinkError;
204
+ /**
205
+ * The PDF is encrypted and no password was supplied (#258).
206
+ */
207
+ static pdfPasswordRequired(): NeuroLinkError;
208
+ /**
209
+ * A password was supplied for an encrypted PDF but it was incorrect (#258).
210
+ */
211
+ static pdfIncorrectPassword(): NeuroLinkError;
202
212
  /**
203
213
  * Create an image too large error
204
214
  */
@@ -52,6 +52,8 @@ export const ERROR_CODES = {
52
52
  CSV_PROCESSING_FAILED: "CSV_PROCESSING_FAILED",
53
53
  // PDF validation errors
54
54
  PDF_PAGE_LIMIT_EXCEEDED: "PDF_PAGE_LIMIT_EXCEEDED",
55
+ PDF_PASSWORD_REQUIRED: "PDF_PASSWORD_REQUIRED",
56
+ PDF_INCORRECT_PASSWORD: "PDF_INCORRECT_PASSWORD",
55
57
  // Rate limiter errors
56
58
  RATE_LIMITER_QUEUE_FULL: "RATE_LIMITER_QUEUE_FULL",
57
59
  RATE_LIMITER_QUEUE_TIMEOUT: "RATE_LIMITER_QUEUE_TIMEOUT",
@@ -553,6 +555,32 @@ export class ErrorFactory {
553
555
  },
554
556
  });
555
557
  }
558
+ /**
559
+ * The PDF is encrypted and no password was supplied (#258).
560
+ */
561
+ static pdfPasswordRequired() {
562
+ return new NeuroLinkError({
563
+ code: ERROR_CODES.PDF_PASSWORD_REQUIRED,
564
+ message: "This PDF is password-protected. Supply the password via " +
565
+ "`pdfOptions: { password: '…' }` (SDK) or `--pdf-password` (CLI).",
566
+ category: ErrorCategory.VALIDATION,
567
+ severity: ErrorSeverity.MEDIUM,
568
+ retriable: false,
569
+ });
570
+ }
571
+ /**
572
+ * A password was supplied for an encrypted PDF but it was incorrect (#258).
573
+ */
574
+ static pdfIncorrectPassword() {
575
+ return new NeuroLinkError({
576
+ code: ERROR_CODES.PDF_INCORRECT_PASSWORD,
577
+ message: "The password supplied for this PDF is incorrect. Check the " +
578
+ "`pdfOptions.password` / `--pdf-password` value and try again.",
579
+ category: ErrorCategory.VALIDATION,
580
+ severity: ErrorSeverity.MEDIUM,
581
+ retriable: false,
582
+ });
583
+ }
556
584
  /**
557
585
  * Create an image too large error
558
586
  */
@@ -1011,6 +1011,12 @@ async function processExplicitPdfFiles(options, maxSize, provider) {
1011
1011
  buffer: result.content,
1012
1012
  filename,
1013
1013
  pageCount: result.metadata?.estimatedPages ?? null,
1014
+ // #258: carry the password so the image-fallback conversion can
1015
+ // decrypt an encrypted PDF for providers without native PDF support.
1016
+ password: options.pdfOptions?.password,
1017
+ // #260: carry the per-page canvas-pixel ceiling so the caller can
1018
+ // raise (or lower) the memory guard for the image-fallback render.
1019
+ maxCanvasPixels: options.pdfOptions?.maxCanvasPixels,
1014
1020
  });
1015
1021
  logger.info(`[PDF] ✅ Queued for multimodal: ${filename} (${result.metadata?.estimatedPages ?? "unknown"} pages)`);
1016
1022
  }
@@ -1103,7 +1109,13 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1103
1109
  // Check if this is a multimodal request
1104
1110
  const hasImages = (inp.images && inp.images.length > 0) ||
1105
1111
  (inp.content && inp.content.some((c) => c.type === "image"));
1106
- const hasPDFs = pdfFiles.length > 0;
1112
+ // A PDF supplied only via input.content (type: "pdf", no explicit
1113
+ // input.pdfFiles and no image alongside it) must still route through the
1114
+ // multimodal path below — otherwise it silently falls through to the
1115
+ // text-only branch and the PDF (and any pdfOptions) never reaches
1116
+ // convertContentToProviderFormat at all.
1117
+ const hasPDFs = pdfFiles.length > 0 ||
1118
+ !!(inp.content && inp.content.some((c) => c.type === "pdf"));
1107
1119
  // If no images or PDFs, use standard message building and convert to MultimodalChatMessage[]
1108
1120
  if (!hasImages && !hasPDFs) {
1109
1121
  // #289: CSV content[] items don't need vision, so they never reach the
@@ -1139,8 +1151,11 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1139
1151
  `Supported providers: ${ProviderImageAdapter.getVisionProviders().join(", ")}`);
1140
1152
  }
1141
1153
  const messages = [];
1142
- // Build enhanced system prompt
1143
- const systemPrompt = buildMultimodalSystemPrompt(options, pdfFiles.length > 0);
1154
+ // Build enhanced system prompt. Gate on the same `hasPDFs` predicate used
1155
+ // for routing above — a PDF supplied only via input.content (no explicit
1156
+ // input.pdfFiles) must still get the "treat inlined content as an
1157
+ // attachment" instruction, or the model can claim no files were attached.
1158
+ const systemPrompt = buildMultimodalSystemPrompt(options, hasPDFs);
1144
1159
  if (systemPrompt.trim()) {
1145
1160
  messages.push({
1146
1161
  role: "system",
@@ -1206,7 +1221,7 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1206
1221
  try {
1207
1222
  let userContent;
1208
1223
  if (inp.content && inp.content.length > 0) {
1209
- userContent = await convertContentToProviderFormat(inp.content, provider, model);
1224
+ userContent = await convertContentToProviderFormat(inp.content, provider, model, options.pdfOptions);
1210
1225
  }
1211
1226
  else if ((inp.images && inp.images.length > 0) || pdfFiles.length > 0) {
1212
1227
  userContent = await convertMultimodalToProviderFormat(inp.text ?? "", inp.images || [], pdfFiles, provider, model);
@@ -1285,7 +1300,7 @@ async function appendCsvContentToText(csvItems, baseText) {
1285
1300
  /**
1286
1301
  * Convert advanced content format to provider-specific format
1287
1302
  */
1288
- async function convertContentToProviderFormat(content, provider, _model) {
1303
+ async function convertContentToProviderFormat(content, provider, _model, pdfOptions) {
1289
1304
  const textContent = content.find((c) => c.type === "text");
1290
1305
  const imageContent = content.filter((c) => c.type === "image");
1291
1306
  const pdfContent = content.filter((c) => c.type === "pdf");
@@ -1312,6 +1327,11 @@ async function convertContentToProviderFormat(content, provider, _model) {
1312
1327
  buffer: typeof pdf.data === "string" ? Buffer.from(pdf.data, "base64") : pdf.data,
1313
1328
  filename: pdf.metadata?.filename || "document.pdf",
1314
1329
  pageCount: pdf.metadata?.pages ?? null,
1330
+ // #258/#260: carry password + canvas-pixel ceiling so a PDF supplied via
1331
+ // the advanced `input.content` array gets the same decryption/memory
1332
+ // guard as the `input.pdfFiles` path (see `processExplicitPdfFiles`).
1333
+ password: pdfOptions?.password,
1334
+ maxCanvasPixels: pdfOptions?.maxCanvasPixels,
1315
1335
  }));
1316
1336
  return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model);
1317
1337
  }
@@ -1694,6 +1714,10 @@ async function convertMultimodalToProviderFormat(text, images, pdfFiles, provide
1694
1714
  const conversionResult = await PDFImageConverter.convertToImages(pdf.buffer, {
1695
1715
  scale: 2.0, // High quality for OCR/analysis
1696
1716
  maxPages: 20, // Limit pages to prevent token overflow
1717
+ ...(pdf.password ? { password: pdf.password } : {}), // #258
1718
+ ...(pdf.maxCanvasPixels
1719
+ ? { maxCanvasPixels: pdf.maxCanvasPixels }
1720
+ : {}), // #260
1697
1721
  });
1698
1722
  logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
1699
1723
  // Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
@@ -1715,6 +1739,13 @@ async function convertMultimodalToProviderFormat(text, images, pdfFiles, provide
1715
1739
  catch (error) {
1716
1740
  const errorMessage = error instanceof Error ? error.message : String(error);
1717
1741
  logger.error(`[PDF→Image] ❌ Failed to convert ${pdf.filename}: ${errorMessage}`);
1742
+ // #258: password errors are already actionable typed errors — re-throw
1743
+ // them unwrapped so the "supply a password" guidance isn't buried.
1744
+ const code = error?.code;
1745
+ if (code === "PDF_PASSWORD_REQUIRED" ||
1746
+ code === "PDF_INCORRECT_PASSWORD") {
1747
+ throw error;
1748
+ }
1718
1749
  // Re-throw so the user knows PDF processing failed
1719
1750
  throw new Error(`PDF to image conversion failed for ${pdf.filename}: ${errorMessage}. ` +
1720
1751
  `Provider ${provider} doesn't support native PDFs and image conversion failed.`, { cause: error });
@@ -51,6 +51,10 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
51
51
  pdfFiles: (string | Buffer<ArrayBufferLike>)[] | undefined;
52
52
  };
53
53
  csvOptions: import("../types/file.js").CSVProcessorOptions | undefined;
54
+ pdfOptions: {
55
+ password?: string;
56
+ maxCanvasPixels?: number;
57
+ } | undefined;
54
58
  systemPrompt: string | undefined;
55
59
  conversationHistory: import("../types/conversation.js").ChatMessage[] | undefined;
56
60
  provider: string;
@@ -51,6 +51,7 @@ export function buildMultimodalOptions(options, providerName, modelName) {
51
51
  pdfFiles: options.input?.pdfFiles,
52
52
  },
53
53
  csvOptions: options.csvOptions,
54
+ pdfOptions: options.pdfOptions,
54
55
  systemPrompt: options.systemPrompt,
55
56
  conversationHistory: options.conversationMessages,
56
57
  provider: providerName,
@@ -21,6 +21,21 @@ export declare class PDFProcessor {
21
21
  private static isValidPDF;
22
22
  private static extractBasicMetadata;
23
23
  static estimateTokens(pageCount: number, mode?: "text-only" | "visual"): number;
24
+ /**
25
+ * Estimate the largest page's rendered pixel count from the PDF's MediaBox
26
+ * entries, WITHOUT rendering (#260). Parses `/MediaBox [llx lly urx ury]`
27
+ * (dimensions in points); rendered pixels ≈ (width·scale)·(height·scale).
28
+ * Returns 0 when no plaintext MediaBox is found (e.g. compressed object
29
+ * streams) — the caller then skips downscaling, matching prior behavior.
30
+ *
31
+ * A crafted/malformed MediaBox can have finite but astronomically large
32
+ * width/height; `width * height * scale * scale` can then overflow past
33
+ * `Number.MAX_VALUE` to `Infinity`. Clamp the product to
34
+ * `Number.MAX_SAFE_INTEGER` so it stays finite — a large-but-finite pixel
35
+ * estimate still triggers downscaling, whereas `Infinity` would collapse
36
+ * the computed downscale factor to 0 (see `convertToImages`).
37
+ */
38
+ private static largestPagePixels;
24
39
  /**
25
40
  * Convert a PDF buffer to an array of base64 PNG images
26
41
  *
@@ -231,6 +231,40 @@ export class PDFProcessor {
231
231
  // ============================================================================
232
232
  // PDF → Image Conversion (for providers without native PDF support)
233
233
  // ============================================================================
234
+ /**
235
+ * Estimate the largest page's rendered pixel count from the PDF's MediaBox
236
+ * entries, WITHOUT rendering (#260). Parses `/MediaBox [llx lly urx ury]`
237
+ * (dimensions in points); rendered pixels ≈ (width·scale)·(height·scale).
238
+ * Returns 0 when no plaintext MediaBox is found (e.g. compressed object
239
+ * streams) — the caller then skips downscaling, matching prior behavior.
240
+ *
241
+ * A crafted/malformed MediaBox can have finite but astronomically large
242
+ * width/height; `width * height * scale * scale` can then overflow past
243
+ * `Number.MAX_VALUE` to `Infinity`. Clamp the product to
244
+ * `Number.MAX_SAFE_INTEGER` so it stays finite — a large-but-finite pixel
245
+ * estimate still triggers downscaling, whereas `Infinity` would collapse
246
+ * the computed downscale factor to 0 (see `convertToImages`).
247
+ */
248
+ static largestPagePixels(pdfBuffer, scale) {
249
+ const text = pdfBuffer.toString("latin1");
250
+ const re = /\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s*\]/g;
251
+ let max = 0;
252
+ let match;
253
+ while ((match = re.exec(text)) !== null) {
254
+ const width = Math.abs(parseFloat(match[3]) - parseFloat(match[1]));
255
+ const height = Math.abs(parseFloat(match[4]) - parseFloat(match[2]));
256
+ if (Number.isFinite(width) && Number.isFinite(height)) {
257
+ const pixels = width * height * scale * scale;
258
+ const boundedPixels = Number.isFinite(pixels)
259
+ ? Math.min(pixels, Number.MAX_SAFE_INTEGER)
260
+ : Number.MAX_SAFE_INTEGER;
261
+ if (boundedPixels > max) {
262
+ max = boundedPixels;
263
+ }
264
+ }
265
+ }
266
+ return max;
267
+ }
234
268
  /**
235
269
  * Convert a PDF buffer to an array of base64 PNG images
236
270
  *
@@ -257,7 +291,7 @@ export class PDFProcessor {
257
291
  */
258
292
  static async convertToImages(pdfBuffer, options) {
259
293
  const startTime = Date.now();
260
- const { scale = 2, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", } = options || {};
294
+ const { scale = 2, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", maxCanvasPixels = PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS, password, } = options || {};
261
295
  const images = [];
262
296
  const warnings = [];
263
297
  // ============================================================================
@@ -273,6 +307,11 @@ export class PDFProcessor {
273
307
  if (!Number.isFinite(scale) || scale <= 0 || scale > PDF_LIMITS.MAX_SCALE) {
274
308
  throw new Error(`Invalid scale: ${scale}. Scale must be a finite number greater than 0 and at most ${PDF_LIMITS.MAX_SCALE}.`);
275
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
+ }
276
315
  // 1. Validate buffer is not empty or too small
277
316
  if (!pdfBuffer || pdfBuffer.length < 5) {
278
317
  throw new Error("Invalid PDF: Buffer is too small or empty. " +
@@ -303,8 +342,38 @@ export class PDFProcessor {
303
342
  scale,
304
343
  maxPages: maxPages || "all",
305
344
  });
306
- // Create PDF document iterator
307
- const document = await pdf(pdfBuffer, { scale });
345
+ // #260: pre-flight page-size check WITHOUT rendering. pdf-to-img applies
346
+ // `scale` uniformly with no per-page hook and no pixel guard, so a very
347
+ // large page (e.g. an architectural drawing with a huge MediaBox) can
348
+ // allocate gigabytes of canvas. Read the largest MediaBox from the PDF
349
+ // bytes and, if that page at the requested scale would exceed
350
+ // maxCanvasPixels, downscale the whole render uniformly to stay under it.
351
+ let effectiveScale = scale;
352
+ const largestPixels = PDFProcessor.largestPagePixels(pdfBuffer, scale);
353
+ if (largestPixels > maxCanvasPixels) {
354
+ const downscale = Math.sqrt(maxCanvasPixels / largestPixels);
355
+ // Floor the result: an astronomically large (but now finite, see
356
+ // `largestPagePixels`) pixel estimate would otherwise push `downscale`
357
+ // — and therefore `effectiveScale` — toward 0, handing `pdf-to-img` a
358
+ // degenerate viewport instead of a small-but-renderable page.
359
+ effectiveScale = Math.max(PDF_LIMITS.MIN_EFFECTIVE_SCALE, scale * downscale);
360
+ // Recompute the ratio actually applied (may differ from `downscale`
361
+ // when the floor above kicks in) so the logged estimate stays honest.
362
+ const actualDownscale = effectiveScale / scale;
363
+ const beforeMB = (largestPixels * 4) / (1024 * 1024);
364
+ const afterMB = (largestPixels * actualDownscale * actualDownscale * 4) /
365
+ (1024 * 1024);
366
+ const msg = `Downscaled render (scale ${scale} → ${effectiveScale.toFixed(3)}): ` +
367
+ `the largest page would allocate ~${beforeMB.toFixed(0)}MB, above the ` +
368
+ `maxCanvasPixels ceiling; reduced to ~${afterMB.toFixed(0)}MB per page.`;
369
+ logger.warn(`[PDF→Image] ⚠️ ${msg}`);
370
+ warnings.push(msg);
371
+ }
372
+ // Create PDF document iterator (password forwarded for encrypted PDFs, #258)
373
+ const document = await pdf(pdfBuffer, {
374
+ scale: effectiveScale,
375
+ ...(password ? { password } : {}),
376
+ });
308
377
  let pageIndex = 0;
309
378
  // Iterate through pages and convert to base64
310
379
  for await (const page of document) {
@@ -346,6 +415,17 @@ export class PDFProcessor {
346
415
  error: errorMessage,
347
416
  conversionTimeMs,
348
417
  });
418
+ // #258: map pdfjs's PasswordException to an actionable typed error so a
419
+ // caller learns to supply (or correct) the password instead of seeing a
420
+ // generic "conversion failed". pdfjs code 1 = NEED_PASSWORD, 2 = INCORRECT.
421
+ const pdfErr = error;
422
+ if (pdfErr?.name === "PasswordException" ||
423
+ /password/i.test(errorMessage)) {
424
+ const incorrect = pdfErr.code === 2 || /incorrect|invalid/i.test(errorMessage);
425
+ throw incorrect
426
+ ? ErrorFactory.pdfIncorrectPassword()
427
+ : ErrorFactory.pdfPasswordRequired();
428
+ }
349
429
  throw new Error(`PDF to image conversion failed: ${errorMessage}`, {
350
430
  cause: error,
351
431
  });
package/dist/neurolink.js CHANGED
@@ -3899,11 +3899,15 @@ Current user's request: ${currentInput}`;
3899
3899
  evaluationDomain: options.evaluationDomain,
3900
3900
  toolUsageContext: options.toolUsageContext,
3901
3901
  input: options.input,
3902
- // CSV processing options must survive the reconstruction into
3903
- // TextGenerationOptions — the message builder reads them (encoding,
3904
- // formatting, sanitization, parse timeout). Omitting them silently
3905
- // dropped every csvOptions field (incl. the CLI --csv-* flags).
3902
+ // Multimodal file-processing options must survive the reconstruction
3903
+ // into TextGenerationOptions — the message builder reads them downstream
3904
+ // (csv encoding/formatting/sanitization/parse timeout, PDF
3905
+ // password/decryption #258). Omitting them here silently dropped e.g.
3906
+ // every csvOptions field (incl. the CLI --csv-* flags) or
3907
+ // pdfOptions.password so the CLI --pdf-password / SDK pdfOptions never
3908
+ // reached convertToImages.
3906
3909
  csvOptions: options.csvOptions,
3910
+ pdfOptions: options.pdfOptions,
3907
3911
  region: options.region,
3908
3912
  tts: options.tts,
3909
3913
  stt: options.stt,