From bdf15b9bd5bf460d092bdf97e49f8fb02bb9c365 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:02:26 +0800 Subject: [PATCH] refactor(javascript): share the reconstruction input contract Remove the reconstruction-only request schema and consume the canonical parsed JavaScript application input. Production already owns parsing at the service boundary; direct test seams now use that same schema instead of a weaker path contract. Preserve format, filesystem, source selection and lifecycle validation. Add a NUL-path regression for canonical admission and typed input-error projection. Verified 63 focused cases plus static, build, docs, discovery and duplication checks; no target execution or real-provider claims. --- .../JavaScriptArtifactReconstruction.ts | 18 +++++------ .../JavaScriptArtifactReconstructionInput.ts | 18 ----------- .../javascriptArtifactReconstruction.test.ts | 30 +++++++++++++++++++ .../javascriptNestedReaderCleanup.test.ts | 5 +++- tests/support/javascriptApplicationScope.ts | 9 ++++-- 5 files changed, 49 insertions(+), 31 deletions(-) delete mode 100644 src/application/javascript/JavaScriptArtifactReconstructionInput.ts diff --git a/src/application/javascript/JavaScriptArtifactReconstruction.ts b/src/application/javascript/JavaScriptArtifactReconstruction.ts index c447cf1c61..a8347faa73 100644 --- a/src/application/javascript/JavaScriptArtifactReconstruction.ts +++ b/src/application/javascript/JavaScriptArtifactReconstruction.ts @@ -20,8 +20,11 @@ import { createJavaScriptArtifactReader as createReader } from "../../artifacts/ import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js"; import type { JavaScriptSourceSelection } from "../../domain/javascript/javascriptSourceSelection.js"; -import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js"; -import { parseOwnedJavaScriptApplicationAnalysisSteps } from "../../domain/javascript/javascriptApplicationAnalysis.js"; +import { + parseOwnedJavaScriptApplicationAnalysisSteps, + type AnalyzeJavaScriptApplicationInput, + type ElectronBoundarySummary, +} from "../../domain/javascript/javascriptApplicationAnalysis.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; @@ -38,10 +41,6 @@ import { JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS } from "../../domain/javascript/j import { createOwnedJavaScriptApplicationEvidenceCooperatively } from "./JavaScriptApplicationEvidence.js"; import { readJavaScriptArtifactFiles } from "../../artifacts/javascript/JavaScriptArtifactFiles.js"; import { buildImmutableJavaScriptArtifactGraphSteps } from "./JavaScriptArtifactGraphBuilder.js"; -import { - javascriptArtifactReconstructionInputSchema, - type JavaScriptArtifactReconstructionInput, -} from "./JavaScriptArtifactReconstructionInput.js"; import { scanCanonicalArtifactInventoryInScope } from "../../artifacts/inventory/scanCanonical.js"; import { createJavaScriptNextAppContext } from "./JavaScriptNextAppContext.js"; import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js"; @@ -80,9 +79,9 @@ export interface JavaScriptArtifactReconstructionResult { readonly limitations: readonly string[]; } -/** Reconstruct one local ASAR or extracted directory without executing code. */ +/** Reconstruct a parsed local ASAR/directory request without executing code. */ export const reconstructJavaScriptArtifact = async ( - rawInput: unknown, + input: AnalyzeJavaScriptApplicationInput, scope: ArtifactResourceScope, signal?: AbortSignal, progress?: ProgressReporter, @@ -101,7 +100,6 @@ export const reconstructJavaScriptArtifact = async ( }); await checkpointJavaScriptAnalysis(signal); }; - const input = javascriptArtifactReconstructionInputSchema.parse(rawInput); abortIfNeeded(signal); const { path, metadata } = await resolveSelectedInput(input.input_path); const format = resolveFormat(path, metadata, input); @@ -487,7 +485,7 @@ const selectedInputResolutionMessage = ( const resolveFormat = ( path: string, metadata: Stats, - input: JavaScriptArtifactReconstructionInput, + input: AnalyzeJavaScriptApplicationInput, ): "asar" | "directory" => { const observed = metadata.isDirectory() ? "directory" diff --git a/src/application/javascript/JavaScriptArtifactReconstructionInput.ts b/src/application/javascript/JavaScriptArtifactReconstructionInput.ts deleted file mode 100644 index ca65b72096..0000000000 --- a/src/application/javascript/JavaScriptArtifactReconstructionInput.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from "zod"; -import { artifactIntegrityPolicySchema } from "../../domain/artifactIntegrityPolicy.js"; -import { javaScriptAnalysisResourceControlsSchema } from "../../domain/javascript/javascriptAnalysisResourceControls.js"; -import { javaScriptSourcePathsSchema } from "../../domain/javascript/javascriptSourceSelection.js"; - -/** Local ASAR/directory reconstruction request. */ -export const javascriptArtifactReconstructionInputSchema = z.strictObject({ - input_path: z.string().min(1), - format: z.enum(["auto", "asar", "directory"]).default("auto"), - integrity_policy: artifactIntegrityPolicySchema, - source_paths: javaScriptSourcePathsSchema.optional(), - ...javaScriptAnalysisResourceControlsSchema.shape, -}); - -/** Parsed local reconstruction request. */ -export type JavaScriptArtifactReconstructionInput = z.infer< - typeof javascriptArtifactReconstructionInputSchema ->; diff --git a/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts b/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts index f5db813439..756d92f59f 100644 --- a/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts +++ b/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts @@ -8,6 +8,7 @@ import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { + analyzeJavaScriptApplication, reconstructJavaScriptArtifact, readJavaScriptArtifactFiles, } from "../../support/javascriptApplicationScope.js"; @@ -384,6 +385,35 @@ it("retains every repeated content observation and containment path", async () = }); }, 15_000); +it("rejects NUL paths through the public request contract before reconstruction", async () => { + const input = { input_path: "invalid\u0000.asar" }; + await expect(reconstructJavaScriptArtifact(input)).rejects.toMatchObject({ + name: "ZodError", + issues: [ + { + code: "invalid_format", + path: ["input_path"], + message: "Local filesystem paths cannot contain NUL", + }, + ], + }); + + const result = await analyzeJavaScriptApplication(input); + if (result.ok) throw new Error("Expected invalid input path"); + expect(result.error).toMatchObject({ + _tag: "AnalysisInputError", + operation: "analyze_javascript_application", + issues: [ + { + path: ["input_path"], + reason: "invalid_format", + expected: "regex", + message: "Local filesystem paths cannot contain NUL", + }, + ], + }); +}); + it("rejects traversal from a production reader seam and malformed ASAR containers", async () => { const root = await fixtureDirectory(); const snapshot = await scanArtifactInventory(root); diff --git a/tests/boundary/filesystem/javascriptNestedReaderCleanup.test.ts b/tests/boundary/filesystem/javascriptNestedReaderCleanup.test.ts index 1f86afd179..87dd482d3a 100644 --- a/tests/boundary/filesystem/javascriptNestedReaderCleanup.test.ts +++ b/tests/boundary/filesystem/javascriptNestedReaderCleanup.test.ts @@ -8,6 +8,7 @@ import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js"; import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js"; +import { analyzeJavaScriptApplicationInputSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; it("retains a failed nested reader through blocked work and scope cleanup", async () => { @@ -40,7 +41,9 @@ it("retains a failed nested reader through blocked work and scope cleanup", asyn }, ); - const input = { input_path: bundle }; + const input = analyzeJavaScriptApplicationInputSchema.parse({ + input_path: bundle, + }); const firstFailure = await reconstructJavaScriptArtifact(input, scope).catch( (cause: unknown) => cause, ); diff --git a/tests/support/javascriptApplicationScope.ts b/tests/support/javascriptApplicationScope.ts index fbb3b83422..59b8166849 100644 --- a/tests/support/javascriptApplicationScope.ts +++ b/tests/support/javascriptApplicationScope.ts @@ -1,10 +1,10 @@ import { withArtifactResourceScope } from "../fixtures/artifactInventory.js"; -import type { ExecutionOptions } from "../../src/application/AnalysisProvider.js"; import { analyzeJavaScriptApplication as analyzeOwned, type JavaScriptApplicationOptions, } from "../../src/application/javascript/JavaScriptApplicationService.js"; import { reconstructJavaScriptArtifact as reconstructOwned } from "../../src/application/javascript/JavaScriptArtifactReconstruction.js"; +import { analyzeJavaScriptApplicationInputSchema } from "../../src/domain/javascript/javascriptApplicationAnalysis.js"; import type { ProgressReporter } from "../../src/application/ProgressReporter.js"; import { readJavaScriptArtifactFiles as readOwned } from "../../src/artifacts/javascript/JavaScriptArtifactFiles.js"; import type { ArtifactReader } from "../../src/artifacts/ArtifactReader.js"; @@ -34,5 +34,10 @@ export const reconstructJavaScriptArtifact = ( progress?: ProgressReporter, ) => withArtifactResourceScope((scope) => - reconstructOwned(rawInput, scope, signal, progress), + reconstructOwned( + analyzeJavaScriptApplicationInputSchema.parse(rawInput), + scope, + signal, + progress, + ), );