@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.
- package/CHANGELOG.md +13 -0
- package/dist/browser/neurolink.min.js +363 -363
- package/dist/cli/commands/proxy.d.ts +2 -1
- package/dist/cli/commands/proxy.js +47 -8
- package/dist/lib/processors/archive/ArchiveProcessor.js +73 -5
- package/dist/lib/processors/base/BaseFileProcessor.d.ts +19 -0
- package/dist/lib/processors/base/BaseFileProcessor.js +104 -3
- package/dist/lib/types/processor.d.ts +23 -0
- package/dist/lib/types/proxy.d.ts +8 -0
- package/dist/processors/archive/ArchiveProcessor.js +73 -5
- package/dist/processors/base/BaseFileProcessor.d.ts +19 -0
- package/dist/processors/base/BaseFileProcessor.js +104 -3
- package/dist/types/processor.d.ts +23 -0
- package/dist/types/proxy.d.ts +8 -0
- package/package.json +4 -3
|
@@ -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
|
|
393
|
-
let buffer =
|
|
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
|
-
|
|
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
|
*/
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -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.
|
|
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",
|