Files
rea/src/domain/webModuleTrace.ts
T
DeryFerd 499ac18011 fix(web): reject NUL bytes in web tool local path inputs
Four web tools accepted caller-selected local paths as plain strings, so
a NUL byte passed contract validation and reached filesystem calls. The
services only checked isAbsolute, and isAbsolute admits a NUL, so the
failure surfaced later as a raw ERR_INVALID_ARG_VALUE instead of a named
input constraint. Every other local-input tool already refuses NUL through
the shared local-path schema, and the existing OS-invalid-local-input
harness covers those but not these web tools.

inspect_web_network_capture.capture_path, export_web_scripts.capture_path
and output_directory, trace_web_source_location.manifest_path and
source_map.path, and trace_web_module_imports.manifest_path and
import_map.path now use localPathStringSchema. Each field already required
a nonempty string, so the advertised pattern and minLength stay the same
and only the NUL refusal is added.

Validation: a new schema test rejects NUL in all seven fields while
accepting ordinary absolute paths; the four tools are added to the
OS-invalid-local-input harness and reject NUL through the real advertised
MCP contracts; typecheck, oxlint, oxfmt, and toolSchemaValidity with a
regenerated catalog. Two webScriptExport boundary cases fail identically
with and without this change on this Windows host (symlink EPERM and
private-runtime-root authority), so they are environmental.
2026-10-11 23:32:52 +07:00

126 lines
4.0 KiB
TypeScript

import { z } from "zod";
import { digestSchema } from "./digests.js";
import { localPathStringSchema } from "./localPath.js";
import {
exportedWebScriptSchema,
webScriptExportManifestSchema,
} from "./webScriptExport.js";
/** Inspect one captured script's native ES module relationships. */
export const webModuleTraceInputSchema = z.strictObject({
manifest_path: localPathStringSchema.describe(
"Absolute path to an export_web_scripts manifest",
),
script_index: z
.number()
.int()
.min(0)
.describe("Zero-based index in manifest.scripts"),
importer_url: z
.string()
.min(1)
.optional()
.describe(
"Explicit HTTP(S) module URL; useful when captured inline/base context is unknown",
),
import_map: z
.strictObject({
path: localPathStringSchema.describe(
"Absolute local import-map JSON path; no fetch",
),
base_url: z
.string()
.min(1)
.describe("Explicit HTTP(S) base for the selected map"),
})
.optional(),
});
export type WebModuleTraceInput = z.output<typeof webModuleTraceInputSchema>;
/** Byte identity of one selected local artifact. */
export const webModuleFileSchema = z.strictObject({
path: z.string().min(1),
sha256: digestSchema,
bytes: z.number().int().min(0),
});
export type WebModuleFile = z.output<typeof webModuleFileSchema>;
const positionSchema = z.strictObject({
offset: z.number().int().min(0),
line: z.number().int().min(1),
column: z.number().int().min(0),
});
/** Parser observations do not assert that a dependency was loaded. */
export const webModuleImportSchema = z.strictObject({
kind: z.enum(["static-import", "re-export", "dynamic-import"]),
specifier: z.string().nullable(),
expression: z.string(),
start: positionSchema,
end: positionSchema,
});
export type WebModuleImport = z.output<typeof webModuleImportSchema>;
/** Native resolver output, including the engine's exact failure text. */
export const webModuleResolutionSchema = z.discriminatedUnion("state", [
z.strictObject({ state: z.literal("resolved"), url: z.string().min(1) }),
z.strictObject({ state: z.literal("rejected"), message: z.string() }),
]);
export type WebModuleResolution = z.output<typeof webModuleResolutionSchema>;
const candidateSchema = z.strictObject({
script_index: z.number().int().min(0),
match: z.enum(["exact-reported-url", "response-url-without-fragment"]),
script: exportedWebScriptSchema,
});
/** Complete outgoing imports with captured candidates and explicit unknowns. */
export const webModuleTraceResultSchema = z.strictObject({
manifest: webModuleFileSchema.extend({
reported_output_directory: z.string(),
...webScriptExportManifestSchema.pick({
capture_path: true,
capture_sha256: true,
capture_kind: true,
capture_completeness: true,
source_evidence_id: true,
}).shape,
}),
source: webModuleFileSchema.extend({
script_index: z.number().int().min(0),
script: exportedWebScriptSchema,
}),
importer: z.strictObject({
url: z.string().min(1),
basis: z.enum(["caller-selected", "reported-source-url"]),
}),
import_map: webModuleFileSchema
.extend({ base_url: z.string().min(1) })
.nullable(),
parser: z.strictObject({
state: z.enum(["parsed", "unparseable"]),
diagnostics: z.array(z.string()),
location_units: z.literal("UTF-16 offsets and columns; one-based lines"),
}),
engine: z
.strictObject({ id: z.string().min(1), version: z.string().min(1) })
.nullable(),
imports: z.array(
webModuleImportSchema.extend({
resolution: z.union([
webModuleResolutionSchema,
z.strictObject({
state: z.literal("unknown"),
reason: z.literal("computed-specifier"),
}),
]),
captured_candidates: z.array(candidateSchema),
execution: z.literal("unknown"),
}),
),
diagnostics: z.array(z.string()),
limitations: z.array(z.string()),
});
export type WebModuleTraceResult = z.output<typeof webModuleTraceResultSchema>;