@juspay/neurolink 10.12.2 → 10.12.4

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.
@@ -52,6 +52,30 @@ import { tracers } from "../../telemetry/tracers.js";
52
52
  import { createFileError, extractHttpStatus, FileErrorCode, isRetryableError, } from "../errors/index.js";
53
53
  import { DEFAULT_RETRY_CONFIG } from "../../types/index.js";
54
54
  const gunzipAsync = promisify(gunzip);
55
+ /**
56
+ * Marker on the error raised when a download is refused for exceeding the
57
+ * processor's size limit.
58
+ *
59
+ * A code rather than a message match, for two reasons. `classifyDownloadError`
60
+ * can then report FILE_TOO_LARGE — the same verdict the post-download check
61
+ * gave, so refusing earlier does not quietly downgrade the error a caller
62
+ * sees to DOWNLOAD_FAILED. And `isRetryableError` decides from text; a bound
63
+ * that fires is not a transient fault, and re-fetching a file that will be
64
+ * exactly as oversized next time is the one thing worth not doing three times.
65
+ */
66
+ const DOWNLOAD_TOO_LARGE_CODE = "DOWNLOAD_TOO_LARGE";
67
+ function downloadTooLargeError(maxSizeMB, typeName) {
68
+ const error = new Error(`Download exceeds the maximum size of ${maxSizeMB} MB for ${typeName}`);
69
+ error.code = DOWNLOAD_TOO_LARGE_CODE;
70
+ return error;
71
+ }
72
+ function isDownloadTooLarge(error) {
73
+ return (error?.code === DOWNLOAD_TOO_LARGE_CODE);
74
+ }
75
+ /** Node's signal that a zlib output bound was reached. */
76
+ function isBufferTooLargeError(error) {
77
+ return (error?.code === "ERR_BUFFER_TOO_LARGE");
78
+ }
55
79
  /**
56
80
  * Abstract base class for file processors.
57
81
  * Provides common download, validation, and error handling functionality.
@@ -389,8 +413,8 @@ export class BaseFileProcessor {
389
413
  if (contentType && contentType.includes("text/html")) {
390
414
  throw new Error(`Received HTML response instead of file content (Content-Type: ${contentType}). This usually means the download URL returned an error page.`);
391
415
  }
392
- const arrayBuffer = await response.arrayBuffer();
393
- let buffer = Buffer.from(arrayBuffer);
416
+ const maxBytes = this.config.maxSizeMB * 1024 * 1024;
417
+ let buffer = await this.readBodyWithinLimit(response, maxBytes);
394
418
  // Check for gzip encoding and decompress if needed
395
419
  // Only decompress if the data actually starts with gzip magic bytes (0x1f 0x8b)
396
420
  const contentEncoding = response.headers.get("Content-Encoding");
@@ -398,9 +422,16 @@ export class BaseFileProcessor {
398
422
  if (contentEncoding?.toLowerCase().includes("gzip") &&
399
423
  isActuallyGzipped) {
400
424
  try {
401
- buffer = Buffer.from(await gunzipAsync(buffer));
425
+ // Bounded at the decoder. The size check that runs after the download
426
+ // only ever sees what already fits in memory, so a few KB of gzip
427
+ // declaring gigabytes was inflated in full before anything objected —
428
+ // the response passed the "is it small" test on its way in.
429
+ buffer = Buffer.from(await gunzipAsync(buffer, { maxOutputLength: maxBytes }));
402
430
  }
403
431
  catch (gzipError) {
432
+ if (isBufferTooLargeError(gzipError)) {
433
+ throw downloadTooLargeError(this.config.maxSizeMB, this.config.fileTypeName);
434
+ }
404
435
  throw new Error(`Failed to decompress gzip response: ${gzipError instanceof Error ? gzipError.message : String(gzipError)}`, { cause: gzipError });
405
436
  }
406
437
  }
@@ -408,7 +439,68 @@ export class BaseFileProcessor {
408
439
  }
409
440
  finally {
410
441
  clearTimeout(timeoutId);
442
+ // Release the connection on every early exit. Rejecting a response for
443
+ // its status or its Content-Type leaves the body unread, which keeps the
444
+ // socket out of the pool until the response is garbage-collected. After
445
+ // a successful read there is nothing left in flight, so this is a no-op.
446
+ controller.abort();
447
+ }
448
+ }
449
+ /**
450
+ * Read a response body, refusing it the moment it passes `maxBytes`.
451
+ *
452
+ * `response.arrayBuffer()` buffers whatever the server sends before anyone
453
+ * can object, so the size check further up only ever ran against a body that
454
+ * had already been paid for in full. Metering the stream makes the ceiling
455
+ * the configured limit instead of the server's choice, and cancelling the
456
+ * reader stops the transfer rather than merely stopping our interest in it.
457
+ *
458
+ * Content-Length is deliberately not trusted as the bound — it is a hint that
459
+ * can be absent or a lie — but it is worth honouring when present, because it
460
+ * refuses the honest oversized case without reading a byte.
461
+ *
462
+ * @param response - Fetch response whose body should be read
463
+ * @param maxBytes - Ceiling for the body, in bytes
464
+ * @returns The body as a Buffer
465
+ * @throws When the body exceeds `maxBytes`
466
+ */
467
+ async readBodyWithinLimit(response, maxBytes) {
468
+ const declared = Number(response.headers.get("Content-Length"));
469
+ if (Number.isFinite(declared) && declared > maxBytes) {
470
+ // Release the socket. An unread body keeps the connection out of the
471
+ // pool until the response is collected, so a run of oversized downloads
472
+ // would hold one socket open each.
473
+ await response.body?.cancel();
474
+ throw downloadTooLargeError(this.config.maxSizeMB, this.config.fileTypeName);
475
+ }
476
+ // Some responses carry no readable stream (mocked fetch, polyfills). The
477
+ // buffered read is the fallback, still checked — just after the fact.
478
+ if (!response.body) {
479
+ const buffered = Buffer.from(await response.arrayBuffer());
480
+ if (buffered.length > maxBytes) {
481
+ throw downloadTooLargeError(this.config.maxSizeMB, this.config.fileTypeName);
482
+ }
483
+ return buffered;
484
+ }
485
+ const reader = response.body.getReader();
486
+ const chunks = [];
487
+ let total = 0;
488
+ for (;;) {
489
+ const { done, value } = await reader.read();
490
+ if (done) {
491
+ break;
492
+ }
493
+ if (!value) {
494
+ continue;
495
+ }
496
+ total += value.byteLength;
497
+ if (total > maxBytes) {
498
+ await reader.cancel();
499
+ throw downloadTooLargeError(this.config.maxSizeMB, this.config.fileTypeName);
500
+ }
501
+ chunks.push(Buffer.from(value));
411
502
  }
503
+ return Buffer.concat(chunks, total);
412
504
  }
413
505
  /**
414
506
  * Download file with retry logic for transient failures.
@@ -559,6 +651,15 @@ export class BaseFileProcessor {
559
651
  * @returns Structured file processing error
560
652
  */
561
653
  classifyDownloadError(error) {
654
+ // Refusing an oversized body mid-stream is the same verdict the old
655
+ // post-download check gave, so it must carry the same code — otherwise
656
+ // bounding the read earlier would silently reclassify a familiar error.
657
+ if (isDownloadTooLarge(error)) {
658
+ return this.createError(FileErrorCode.FILE_TOO_LARGE, {
659
+ maxMB: this.config.maxSizeMB,
660
+ type: this.config.fileTypeName,
661
+ }, error);
662
+ }
562
663
  if (isAbortError(error)) {
563
664
  return this.createError(FileErrorCode.DOWNLOAD_TIMEOUT, { timeoutMs: this.config.timeoutMs }, error);
564
665
  }
@@ -761,6 +761,29 @@ export type ArchiveDecompressionResult = {
761
761
  } | {
762
762
  readonly status: "failed";
763
763
  };
764
+ /**
765
+ * Outcome of reading one ZIP entry under an explicit output bound.
766
+ *
767
+ * Separate from {@link ArchiveDecompressionResult} because the failures differ:
768
+ * a single-stream archive can fail for want of an external tool, while a ZIP
769
+ * entry can instead use a compression method this reader does not implement.
770
+ * Collapsing them would force one caller to handle a state it can never see.
771
+ *
772
+ * `too-large` is a distinct outcome rather than a corrupt-file error because it
773
+ * is a verdict about our limit, not about the archive: the entry may be
774
+ * perfectly well-formed, and telling the user it is damaged would send them to
775
+ * re-create a file that was never broken.
776
+ */
777
+ export type ArchiveEntryReadResult = {
778
+ readonly status: "ok";
779
+ readonly buffer: Buffer;
780
+ } | {
781
+ readonly status: "too-large";
782
+ } | {
783
+ readonly status: "unsupported-method";
784
+ } | {
785
+ readonly status: "corrupt";
786
+ };
764
787
  /**
765
788
  * Metadata about an individual entry within an archive.
766
789
  */
@@ -1861,6 +1861,14 @@ export type UpdateCheckResult = {
1861
1861
  latestVersion: string;
1862
1862
  updateAvailable: boolean;
1863
1863
  };
1864
+ /** Result of one local proxy health probe by the updater or fail-open guard. */
1865
+ export type ProxyHealthProbe = {
1866
+ healthy: boolean;
1867
+ durationMs: number;
1868
+ failure: "http_status" | "network" | "timeout" | null;
1869
+ statusCode: number | null;
1870
+ errorCode: string | null;
1871
+ };
1864
1872
  /** Parsed major.minor.patch components of a semver string. */
1865
1873
  export type SemVer = {
1866
1874
  major: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "10.12.2",
3
+ "version": "10.12.4",
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": {
@@ -163,7 +163,7 @@
163
163
  "test:system-messages": "npx tsx test/continuous-test-suite-system-messages.ts",
164
164
  "test:test-stubs": "npx tsx test/continuous-test-suite-test-stubs.ts",
165
165
  "test:tool-routing-semantic": "npx tsx test/continuous-test-suite-tool-routing-semantic.ts",
166
- "test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:file-detector-extension && pnpm run test:file-detector-magic-bytes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:litellm-context && pnpm run test:dedup-execute-map && pnpm run test:step-budget-guard && pnpm run test:agent-plumbing && pnpm run test:tool-execution-recorder && pnpm run test:proxy-terminal-errors && pnpm run test:proxy-usage-refresh && pnpm run test:system-messages && pnpm run test:tool-routing-semantic && pnpm run test:anthropic-tools-policy && pnpm run test:anthropic-structured && pnpm run test:sagemaker-tools && pnpm run test:anthropic-multimodal && pnpm run test:excel-interop && pnpm run test:model-capabilities && pnpm run test:agent-runtime:vitest && pnpm run test:agent-delegation && pnpm run test:retry-after:vitest && pnpm run test:sampling-params && pnpm run test:structured-recovery && pnpm run test:prompt-redaction && pnpm run test:mcp-result-cache && pnpm run test:test-stubs && pnpm run test:model-not-found-retryable && pnpm run test:websearch-grounding",
166
+ "test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:file-detector-extension && pnpm run test:file-detector-magic-bytes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:litellm-context && pnpm run test:dedup-execute-map && pnpm run test:step-budget-guard && pnpm run test:agent-plumbing && pnpm run test:tool-execution-recorder && pnpm run test:proxy-terminal-errors && pnpm run test:proxy-usage-refresh && pnpm run test:system-messages && pnpm run test:tool-routing-semantic && pnpm run test:anthropic-tools-policy && pnpm run test:anthropic-structured && pnpm run test:sagemaker-tools && pnpm run test:anthropic-multimodal && pnpm run test:excel-interop && pnpm run test:model-capabilities && pnpm run test:agent-runtime:vitest && pnpm run test:agent-delegation && pnpm run test:retry-after:vitest && pnpm run test:sampling-params && pnpm run test:structured-recovery && pnpm run test:prompt-redaction && pnpm run test:mcp-result-cache && pnpm run test:test-stubs && pnpm run test:model-not-found-retryable && pnpm run test:websearch-grounding && pnpm run test:archive:security",
167
167
  "// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit)": "",
168
168
  "test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic",
169
169
  "// CI tier — product output (image/video/TTS/PPT) — costs $$ per run": "",
@@ -245,7 +245,8 @@
245
245
  "test:sampling-params": "npx tsx test/continuous-test-suite-sampling-params.ts",
246
246
  "test:structured-recovery": "npx tsx test/continuous-test-suite-structured-recovery.ts",
247
247
  "test:prompt-redaction": "npx tsx test/continuous-test-suite-prompt-redaction.ts",
248
- "test:mcp-result-cache": "npx tsx test/continuous-test-suite-mcp-result-cache.ts"
248
+ "test:mcp-result-cache": "npx tsx test/continuous-test-suite-mcp-result-cache.ts",
249
+ "test:archive:security": "npx tsx test/continuous-test-suite-archive-security.ts"
249
250
  },
250
251
  "files": [
251
252
  "dist",