mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(cli): validate integer tokens before coercion
Validate decimal tokens at the CLI owner before safe-number parsing. Apply the same rule to positional indexes/generations and repeated record selectors, retaining leading-zero decimals and canonical MCP inputs. Refs #4182 Co-authored-by: Arjun Prabhulal <arjun.prabhulal@gmail.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import { logCliCommand } from "../cliLogging.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { cliIntegerArgument } from "./options.js";
|
||||
|
||||
/** Register commands that control an already-running Cutter instance. */
|
||||
export const registerCutterCommands = (
|
||||
@@ -33,11 +34,9 @@ export const registerCutterCommands = (
|
||||
.string()
|
||||
.uuid()
|
||||
.describe("Session ID copied from list-cutter-sessions."),
|
||||
expectedGeneration: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.describe("Session generation returned by list-cutter-sessions."),
|
||||
expectedGeneration: cliIntegerArgument(0).describe(
|
||||
"Session generation returned by list-cutter-sessions.",
|
||||
),
|
||||
command: z
|
||||
.string()
|
||||
.min(1)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from "incur";
|
||||
|
||||
import {
|
||||
executableFormatHintSchema,
|
||||
type ExecutableFormatHint,
|
||||
@@ -11,6 +13,13 @@ import { analysisProviderSelectorSchema } from "../contracts/providerSelection.j
|
||||
import type { AnalysisProviderSelector } from "../contracts/providerSelection.js";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
/** Parse decimal integer tokens before Number coercion can erase their syntax. */
|
||||
export const cliIntegerArgument = (min: number) =>
|
||||
z
|
||||
.string()
|
||||
.regex(/^[0-9]+$/u, "Expected a decimal integer")
|
||||
.pipe(z.coerce.number<string>().safe().min(min));
|
||||
|
||||
export const providerSelectionOption = analysisProviderSelectorSchema
|
||||
.optional()
|
||||
.describe(
|
||||
|
||||
@@ -6,6 +6,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { webModuleTraceInputSchema } from "../domain/webModuleTrace.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { cliIntegerArgument } from "./options.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
|
||||
const importMapInputSchema =
|
||||
@@ -23,11 +24,9 @@ export const registerWebModuleCommands = (
|
||||
"Trace one captured script's native imports to URLs and captured source candidates",
|
||||
args: z.object({
|
||||
manifestPath: webModuleTraceInputSchema.shape.manifest_path,
|
||||
scriptIndex: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.describe("Zero-based manifest script index"),
|
||||
scriptIndex: cliIntegerArgument(0).describe(
|
||||
"Zero-based manifest script index",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
importerUrl: webModuleTraceInputSchema.shape.importer_url,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { cliIntegerArgument } from "./options.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
|
||||
@@ -26,7 +27,7 @@ export const registerWebNetworkCaptureCommands = (
|
||||
}),
|
||||
options: z.object({
|
||||
record: z
|
||||
.array(z.coerce.number().int().nonnegative())
|
||||
.array(cliIntegerArgument(0))
|
||||
.optional()
|
||||
.describe("Zero-based producer ordinals; repeat to select several"),
|
||||
sensitiveValue: z
|
||||
|
||||
@@ -5,6 +5,7 @@ import { logCliCommand } from "../cliLogging.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { cliIntegerArgument } from "./options.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
|
||||
/** Expose the same explicit selected map/point contract through the CLI. */
|
||||
@@ -21,21 +22,17 @@ export const registerWebSourceLocationCommands = (
|
||||
manifestPath: z
|
||||
.string()
|
||||
.describe("Absolute export_web_scripts manifest path"),
|
||||
scriptIndex: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.describe("Zero-based manifest script index"),
|
||||
scriptIndex: cliIntegerArgument(0).describe(
|
||||
"Zero-based manifest script index",
|
||||
),
|
||||
sourceMapPath: z.string().describe("Absolute local source-map path"),
|
||||
sourceMapUrl: z
|
||||
.string()
|
||||
.describe("Absolute source-map URL context; no fetch"),
|
||||
line: z.coerce.number().int().min(1).describe("One-based generated line"),
|
||||
column: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.describe("Zero-based UTF-16 generated column"),
|
||||
line: cliIntegerArgument(1).describe("One-based generated line"),
|
||||
column: cliIntegerArgument(0).describe(
|
||||
"Zero-based UTF-16 generated column",
|
||||
),
|
||||
}),
|
||||
run: ({ args }) =>
|
||||
withCommandCancellation(async (signal) => {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { serveCli } from "./cliServeHarness.js";
|
||||
|
||||
const SESSION_ID = "27d3e3f1-f1e5-49ae-91ec-95af1f343a5a";
|
||||
const NON_INTEGER_TEXT = [
|
||||
"",
|
||||
" ",
|
||||
"\t",
|
||||
"1\n",
|
||||
"0x1",
|
||||
"0b1",
|
||||
"1e0",
|
||||
"+1",
|
||||
"1.0",
|
||||
"9007199254740992",
|
||||
];
|
||||
const NON_INTEGER_OPTION_TEXT = [...NON_INTEGER_TEXT, "-0"];
|
||||
|
||||
const expectCliValidationError = (
|
||||
result: Awaited<ReturnType<typeof serveCli>>,
|
||||
field: string,
|
||||
): void => {
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({
|
||||
code: "VALIDATION_ERROR",
|
||||
fieldErrors: expect.arrayContaining([
|
||||
expect.objectContaining({ path: expect.stringContaining(field) }),
|
||||
]),
|
||||
});
|
||||
};
|
||||
|
||||
describe("CLI integer arguments", () => {
|
||||
it.each(NON_INTEGER_TEXT)(
|
||||
"rejects %j as a Cutter expected generation",
|
||||
async (value) => {
|
||||
const result = await serveCli([
|
||||
"cutter-command",
|
||||
SESSION_ID,
|
||||
value,
|
||||
"pdi",
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expectCliValidationError(result, "expectedGeneration");
|
||||
},
|
||||
);
|
||||
|
||||
it.each(NON_INTEGER_TEXT)(
|
||||
"rejects %j as a web module script index",
|
||||
async (value) => {
|
||||
const result = await serveCli([
|
||||
"trace-web-module-imports",
|
||||
"/nonexistent/manifest.json",
|
||||
value,
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expectCliValidationError(result, "scriptIndex");
|
||||
},
|
||||
);
|
||||
|
||||
it.each(NON_INTEGER_OPTION_TEXT)(
|
||||
"rejects %j in a repeated web capture record selector",
|
||||
async (value) => {
|
||||
const result = await serveCli([
|
||||
"inspect-web-network-capture",
|
||||
"/nonexistent/capture.qlog",
|
||||
"qlog",
|
||||
"--record",
|
||||
"0",
|
||||
"--record",
|
||||
value,
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expectCliValidationError(result, "record");
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the one-based source line lower bound at the CLI boundary", async () => {
|
||||
const result = await serveCli([
|
||||
"trace-web-source-location",
|
||||
"/nonexistent/manifest.json",
|
||||
"0",
|
||||
"/nonexistent/app.js.map",
|
||||
"https://example.test/app.js.map",
|
||||
"0",
|
||||
"0",
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expectCliValidationError(result, "line");
|
||||
});
|
||||
});
|
||||
@@ -169,7 +169,7 @@ describe("Cutter CLI command output flags", () => {
|
||||
);
|
||||
|
||||
const result = await serveCli(
|
||||
["cutter-command", SESSION_ID, "0", "pdi", "--json-output"],
|
||||
["cutter-command", SESSION_ID, "000", "pdi", "--json-output"],
|
||||
{ REA_CUTTER_BRIDGE_DIR: directory },
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user