UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

234 lines • 10.4 kB
/** * CLI Input Validation Utilities * * Validates multimodal file flags (--image/--csv/--pdf/--video/--file), the * batch `<promptsFile>` positional, and --csv-max-rows before a * generate/stream/batch run starts, so bad input fails fast with a clear * message instead of surfacing deep inside provider calls. Exported as * standalone functions (rather than private statics on CLICommandFactory) * so callers — including tests — can import and invoke them directly, * without reaching into the class via an `unknown` cast. */ import fs from "node:fs"; import { fileURLToPath } from "node:url"; import chalk from "chalk"; import { logger } from "../../utils/logger.js"; import { redactUrlForError } from "../../utils/logSanitize.js"; import { resolveFilePath } from "./pathResolver.js"; /** * Soft (warn-only) size thresholds for CLI multimodal flags. Intentionally * defined here rather than reusing `lib/processors/config/sizeLimits.ts`'s * hard processor caps (e.g. PDF's 100MB hard limit): reusing those names/ * values would couple the CLI layer to processor internals across the * CLI/SDK boundary, and would blur the line between a soft "this may be * slow" warning and the actual hard cap the processor enforces. */ export const CLI_SOFT_LIMITS_MB = { IMAGE_MAX_MB: 10, CSV_MAX_MB: 50, PDF_MAX_MB: 100, // Distinct from PDF_MAX_MB above (which mirrors the processor's true // 100MB hard cap in lib/processors/config/sizeLimits.ts and is kept here // for backward compatibility). Docs promise a 50MB *warning* threshold // specifically for --pdf — wire warnAtMB to this value, not the hard cap. PDF_SOFT_MAX_MB: 50, VIDEO_MAX_MB: 500, }; // `file://` is intentionally excluded here: it addresses a local path, so it // must still go through directory/missing-path validation below (via // tryResolveFileUrl in validateSingleLocalPath). Only genuine remote/inline // references (http(s), data URIs) skip validation entirely (#319). function isNonLocalFileReference(filePath) { const lower = filePath.toLowerCase(); return (lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("data:")); } // #291: a `file://` URL still points at something on disk, so validation // needs the real filesystem path. Returns undefined (instead of throwing) // for a malformed URL so the caller can report a friendly error. function tryResolveFileUrl(filePath) { if (!filePath.toLowerCase().startsWith("file://")) { return filePath; } try { return fileURLToPath(filePath); } catch { return undefined; } } /** * Shared aggregated-error format for any invalid local file path (missing, * unreadable, a directory, or a malformed `file://` URL). Used by both the * flag-based loop in `validateCliInputFiles` and the batch `<promptsFile>` * positional check in `validatePromptsFilePath`, so every caller gets the * same clear error + troubleshooting hints instead of a raw exception. */ function buildInvalidFileError(errors) { return new Error("❌ One or more input files are invalid:\n" + `${errors.join("\n")}\n` + "💡 Troubleshooting:\n" + " • Check the path is correct and relative to your current directory, or pass an absolute path / URL.\n" + ` • Check your working directory (${process.cwd()}).\n` + " • For remote resources, prefix the value with http:// or https://.\n" + " • If you pointed at a folder, point at the file inside it instead."); } /** * Validate a single raw path/URL value for `option`, pushing a friendly * message onto `errors` instead of letting fs calls throw a raw exception * (ENOENT, EACCES/EPERM TOCTOU, a directory, or a malformed `file://` URL). * Returns the resolved local filesystem path on success (or the original * value unchanged for a skipped http(s)/data: reference), and `undefined` * when an error was pushed. * * `rawPath` is redacted via `redactUrlForError` before ever being * interpolated into a message — a `file://` value can carry a sensitive * query string, fragment, or embedded `user:pass@` credentials that must * never be echoed back verbatim. This is a no-op for ordinary filesystem * paths (they aren't parseable as a URL, so the redaction fallback simply * returns them unchanged). */ function validateSingleLocalPath(option, rawPath, errors, warnAtMB) { const resolvedPath = resolveFilePath(rawPath); if (isNonLocalFileReference(resolvedPath)) { // URLs / data: refs skip both existence and size checks (#319). return resolvedPath; } const safeRawPath = redactUrlForError(rawPath); // #291: `file://` URLs are local paths in disguise — validate the decoded // filesystem path so a directory or missing path is still caught here // instead of failing cryptically deep in processing. const pathToValidate = tryResolveFileUrl(resolvedPath); if (pathToValidate === undefined) { errors.push(`${option} is not a valid file:// URL: ${safeRawPath}`); return undefined; } const safeResolvedPath = redactUrlForError(pathToValidate); // Single statSync covers existence, directory rejection, and the size // warning below — avoids a second syscall and closes the TOCTOU gap a // separate existsSync()+statSync() pair would leave. let stat; try { stat = fs.statSync(pathToValidate); } catch (err) { // Only ENOENT actually means "not found" — EACCES (permission denied), // ELOOP (symlink loop), ENOTDIR, etc. are real failures that get masked // (and made undebuggable) if we blanket-label every statSync error the // same way. const code = err?.code; if (code === "ENOENT") { errors.push(`${option} path not found: ${safeRawPath} (resolved to ${safeResolvedPath})`); } else { const reason = err instanceof Error ? err.message : String(err); errors.push(`${option} path could not be accessed: ${safeRawPath} (resolved to ${safeResolvedPath}) — ` + `${code ? `${code}: ` : ""}${reason}`); } return undefined; } // #288: reject a directory before it fails cryptically deep in processing // (readFileSync on a dir throws a raw EISDIR). if (stat.isDirectory()) { errors.push(`${option} path is a directory, not a file: ${safeRawPath} (resolved to ${safeResolvedPath})`); return undefined; } // #319: warn (not error) when a local multimodal file is unusually large // so the user isn't surprised by slow processing / token blowups. Safety- // relevant, so always visible (not gated behind --quiet) and written to // stderr — never stdout — so `--format json` output stays parseable. if (warnAtMB !== undefined) { const sizeMB = stat.size / (1024 * 1024); if (sizeMB > warnAtMB) { logger.alwaysStderr(chalk.yellow(`⚠️ ${option} file ${safeRawPath} is ${sizeMB.toFixed(1)}MB ` + `(above the ${warnAtMB}MB soft limit) — processing may be slow ` + `or exceed provider token/size limits.`)); } } return pathToValidate; } /** * Validate --image/--csv/--pdf/--video/--file inputs before a CLI run * starts: every local path (including `file://` URLs) must exist, must not * be a directory, and — for the flags with a soft limit — triggers a size * warning above the threshold. Genuine http(s)/data: URLs skip both * existence and size checks (#319). */ export function validateCliInputFiles(argv) { const fileArgs = [ { option: "--image", value: argv.image, warnAtMB: CLI_SOFT_LIMITS_MB.IMAGE_MAX_MB, }, { option: "--csv", value: argv.csv, warnAtMB: CLI_SOFT_LIMITS_MB.CSV_MAX_MB, }, { option: "--pdf", value: argv.pdf, warnAtMB: CLI_SOFT_LIMITS_MB.PDF_SOFT_MAX_MB, }, { option: "--video", value: argv.video, warnAtMB: CLI_SOFT_LIMITS_MB.VIDEO_MAX_MB, }, { option: "--file", value: argv.file }, ]; const errors = []; for (const { option, value, warnAtMB } of fileArgs) { if (!value) { continue; } const rawPaths = Array.isArray(value) ? value : [value]; for (const rawPath of rawPaths) { validateSingleLocalPath(option, rawPath, errors, warnAtMB); } } if (errors.length > 0) { throw buildInvalidFileError(errors); } } /** * #291: validate the batch `<promptsFile>` positional through the same * aggregated-error-friendly path as the multimodal flags above (not found / * unreadable / directory / malformed `file://` URL), instead of a raw * existsSync+readFileSync that throws an unfriendly EISDIR when pointed at * a directory. The `<file>` label (rather than `--file`) keeps the message * from being confused with the `--file` flag, which batch doesn't register * at all (see `createBatchCommand`). Returns the resolved local path for * the caller to read. */ export function validatePromptsFilePath(rawPath) { const errors = []; const resolved = validateSingleLocalPath("<file>", rawPath, errors); if (errors.length > 0) { throw buildInvalidFileError(errors); } // `errors` is empty here, so `validateSingleLocalPath` did not return // undefined. return resolved; } /** * #310: validate --csv-max-rows is a positive integer in the range * 1-100000, matching the documented option range (the value previously * flowed through silently, or — above 100000 — only warned instead of * being rejected, contradicting the documented hard range). */ export function validateCsvMaxRows(argv) { const raw = argv.csvMaxRows; if (raw === undefined) { return; } const value = Number(raw); if (!Number.isInteger(value) || value < 1 || value > 100000) { throw new Error(`Invalid --csv-max-rows (--csvMaxRows) value: ${String(raw)}. ` + `Must be a positive integer in the range 1-100000. ` + `Example: --csv-max-rows 500`); } } //# sourceMappingURL=inputValidation.js.map